1. 程式人生 > >Linux/Unix 多線程通信

Linux/Unix 多線程通信

堆棧 task 環境 用戶 pos 什麽 markdown 失敗 star

線程間無需特別的手段進行通信,因為線程間可以共享數據結構,也就是一個全局變量可以被兩個線程同時使用。 不過要註意的是線程間需要做好同步,一般用 mutex。 可以參考一些比較新的 UNIX/Linux 編程的書,都會提到 Posix 線程編程,比如《UNIX環境高級編程(第二版)》、《UNIX系統編程》等等。 Linux 的消息屬於 IPC,也就是進程間通信,線程用不上。

  • 使用多線程的理由之一是和進程相比,它是一種非常”節儉”的多任務操作方式。

    我們知道,在 Linux 系統下,啟動一個新的進程必須分配給它獨立的地址空間,建立眾多的數據表來維護它的代碼段、堆棧段和數據段,這是一種”昂貴”的多任務工作方式。 而運行於一個進程中的多個線程,它們彼此之間使用相同的地址空間,共享大部分數據,啟動一個線程所花費的空間遠遠小於啟動一個進程所花費的空間,而且,線程間彼此切換所需的時間也遠遠小於進程間切換所需要的時間。

  • 使用多線程的理由之二是線程間方便的通信機制。

    對不同進程來說,它們具有獨立的數據空間,要進行數據的傳遞只能通過通信的方式進行,這種方式不僅費時,而且很不方便。 線程則不然,由於同一進程下的線程之間共享數據空間,所以一個線程的數據可以直接為其它線程所用,這不僅快捷,而且方便。 當然,數據的共享也帶來其他一些問題,有的變量不能同時被兩個線程所修改,有的子程序中聲明為 static 的數據更有可能給多線程程序帶來災難性的打擊,這些正是編寫多線程程序時最需要註意的地方。

Table of Contents

  • Table of Contents
  • 通信方式
    • 信號
    • 互斥鎖
    • 條件變量
    • 信號量
    • 信號量與線程鎖、條件變量
  • 簡單的多線程程序
    • 修改線程的屬性
    • 線程的數據處理
    • 互斥鎖
    • 信號量

通信方式

信號

Linux 用 pthread_kill 對線程發信號。

Windows 用 PostThreadMessage 進行線程間通信,但實際上極少用這種方法。還是利用同步多一些 LINUX 下的同步和 Windows 原理都是一樣的。不過 Linux 下的 singal 中斷也很好用。

用好信號量,共享資源就可以了。

互斥鎖

互斥鎖,是一種信號量,常用來防止兩個進程或線程在同一時刻訪問相同的共享資源。

需要的頭文件:pthread.h

互斥鎖標識符:pthread_mutex_t

  1. 互斥鎖初始化:

    函數原型: int pthread_mutex_init (pthread_mutex_t* mutex,const pthread_mutexattr_t* mutexattr);

    函數傳入值: mutex:互斥鎖。

    mutexattr:

    • PTHREAD_MUTEX_INITIALIZER 創建快速互斥鎖。
    • PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP 創建遞歸互斥鎖。
    • PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP 創建檢錯互斥鎖。

    函數返回值:成功:0;出錯:-1

  2. 互斥操作函數

    • int pthread_mutex_lock(pthread_mutex_t* mutex); //上鎖
    • int pthread_mutex_trylock (pthread_mutex_t* mutex); //只有在互斥被鎖住的情況下才阻塞
    • int pthread_mutex_unlock (pthread_mutex_t* mutex); //解鎖
    • int pthread_mutex_destroy (pthread_mutex_t* mutex); //清除互斥鎖

    函數傳入值:mutex:互斥鎖。

    函數返回值:成功:0;出錯:-1

    使用形式:

    pthread_mutex_t mutex;
    pthread_mutex_init (&mutex, NULL); /*定義*/
    ...
    pthread_mutex_lock(&mutex); /*獲取互斥鎖*/
    ... /*臨界資源*/
    pthread_mutex_unlock(&mutex); /*釋放互斥鎖*/
    

      

    如果一個線程已經給一個互斥量上鎖了,後來在操作的過程中又再次調用了該上鎖的操作,那麽該線程將會無限阻塞在這個地方,從而導致死鎖。這就需要互斥量的屬性。

    互斥量分為下面三種:

    1. 快速型。這種類型也是默認的類型。該線程的行為正如上面所說的。
    2. 遞歸型。如果遇到我們上面所提到的死鎖情況,同一線程循環給互斥量上鎖,那麽系統將會知道該上鎖行為來自同一線程,那麽就會同意線程給該互斥量上鎖。
    3. 錯誤檢測型。如果該互斥量已經被上鎖,那麽後續的上鎖將會失敗而不會阻塞,pthread_mutex_lock()操作將會返回EDEADLK。

    互斥量的屬性類型為pthread_mutexattr_t。 聲明後調用pthread_mutexattr_init()來創建該互斥量。然後調用 pthread_mutexattr_settype來設置屬性。 格式如下:int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int kind);

    第一個參數attr,就是前面聲明的屬性變量;第二個參數kind,就是我們要設置的屬性類型。他有下面幾個選項:

    • PTHREAD_MUTEX_FAST_NP
    • PTHREAD_MUTEX_RECURSIVE_NP
    • PTHREAD_MUTEX_ERRORCHECK_NP

    下面給出一個使用屬性的簡單過程:

    pthread_mutex_t mutex;
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_RECURSIVE_NP);
    pthread_mutex_init(&mutex,&attr);
    pthread_mutex_destroy(&attr);
    

      

    前面我們提到在調用pthread_mutex_lock()的時候,如果此時mutex已經被其他線程上鎖,那麽該操作將會一直阻塞在這個地方。如果我們此時不想一直阻塞在這個地方,那麽可以調用下面函數:pthread_mutex_trylock。

    如果此時互斥量沒有被上鎖,那麽pthread_mutex_trylock將會返回0,並會對該互斥量上鎖。如果互斥量已經被上鎖,那麽會立刻返回EBUSY。

條件變量

需要的頭文件:pthread.h

條件變量標識符:pthread_cond_t

  1. 互斥鎖的存在問題:

    互斥鎖一個明顯的缺點是它只有兩種狀態:鎖定和非鎖定。設想一種簡單情景:多個線程訪問同一個共享資源時,並不知道何時應該使用共享資源,如果在臨界區裏 加入判斷語句,或者可以有效,但一來效率不高,二來復雜環境下就難以編寫了,這是我們需要一個結構,能在條件成立時觸發相應線程,進行變量修改和訪問。

  2. 條件變量:

    條件變量通過允許線程阻塞和等待另一個線程發送信號的方法彌補了互斥鎖的不足,它常和互斥鎖一起使用。使用時,條件變量被用來阻塞一個線程,當條件不滿足時,線程往往解開相應的互斥鎖並等待條件發生變化。一旦其它的某個線程改變了條件變量,它將通知相應的條件變量喚醒一個或多個正被此條件變量阻塞的線程。 這些線程將重新鎖定互斥鎖並重新測試條件是否滿足。

  3. 條件變量的相關函數

    • pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //條件變量結構
    • int pthread_cond_init(pthread_cond_t cond, pthread_condattr_tcond_attr);
    • int pthread_cond_signal(pthread_cond_t *cond);
    • int pthread_cond_broadcast(pthread_cond_t *cond);
    • int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
    • int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,
    • const struct timespec *abstime);
    • int pthread_cond_destroy(pthread_cond_t *cond);

    詳細說明

  4. 創建和註銷

    條件變量和互斥鎖一樣,都有靜態動態兩種創建方式

    1. 靜態方式

      靜態方式使用PTHREAD_COND_INITIALIZER常量,如下:

      pthread_cond_t cond=PTHREAD_COND_INITIALIZER

    2. 動態方式

      動態方式調用pthread_cond_init()函數,API定義如下:

      int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr)

      盡管POSIX標準中為條件變量定義了屬性,但在LinuxThreads中沒有實現,因此cond_attr值通常為NULL,且被忽略。

      註銷一個條件變量需要調用pthread_cond_destroy(),只有在沒有線程在該條件變量上等待的時候才能註銷這個條件變量,否則返回 EBUSY。因為Linux實現的條件變量沒有分配什麽資源,所以註銷動作只包括檢查是否有等待線程。API定義如下:int pthread_cond_destroy(pthread_cond_t *cond)

  5. 等待和激發

    1. 等待

      • int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) //等待
      • int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,
      • const struct timespec *abstime) //有時等待

      等待條件有兩種方式:無條件等待pthread_cond_wait()和計時等待pthread_cond_timedwait(),其中計時等待方式 如果在給定時刻前條件沒有滿足,則返回ETIMEOUT,結束等待,其中abstime以與time()系統調用相同意義的絕對時間形式出現,0表示格林 尼治時間1970年1月1日0時0分0秒。

      無論哪種等待方式,都必須和一個互斥鎖配合,以防止多個線程同時請求pthread_cond_wait()(或 pthread_cond_timedwait(),下同)的競爭條件(Race Condition)。mutex互斥鎖必須是普通鎖(PTHREAD_MUTEX_TIMED_NP)或者適應鎖 (PTHREAD_MUTEX_ADAPTIVE_NP),且在調用pthread_cond_wait()前必須由本線程加鎖 (pthread_mutex_lock()),而在更新條件等待隊列以前,mutex保持鎖定狀態,並在線程掛起進入等待前解鎖。在條件滿足從而離開 pthread_cond_wait()之前,mutex將被重新加鎖,以與進入pthread_cond_wait()前的加鎖動作對應。

    2. 激發

      激發條件有兩種形式,pthread_cond_signal()激活一個等待該條件的線程,存在多個等待線程時按入隊順序激活其中一個;而pthread_cond_broadcast()則激活所有等待線程。</p>

  6. 其他操作

    pthread_cond_wait ()和pthread_cond_timedwait()都被實現為取消點,因此,在該處等待的線程將立即重新運行,在重新鎖定mutex後離開 pthread_cond_wait(),然後執行取消動作。也就是說如果pthread_cond_wait()被取消,mutex是保持鎖定狀態的, 因而需要定義退出回調函數來為其解鎖。

    pthread_cond_wait實際上可以看作是以下幾個動作的合體:

    解鎖線程鎖;

    等待條件為true;

    加鎖線程鎖;

    使用形式:

    // 線程一代碼
    pthread_mutex_lock(&mutex);
    if (條件滿足)
    pthread_cond_signal(&cond);
    pthread_mutex_unlock(&mutex);
    <p>// 線程二代碼
    pthread_mutex_lock(&mutex);
    while (條件不滿足)
    pthread_cond_wait(&cond, &mutex);
    pthread_mutex_unlock(&mutex);
    /*線程二中為什麽使用while呢?因為在pthread_cond_signal和pthread_cond_wait返回之間,有時間差,假設在這 個時間差內,條件改變了,顯然需要重新檢查條件。也就是說在pthread_cond_wait被喚醒的時候可能該條件已經不成立。*/
    

      

信號量

信號量其實就是一個計數器,也是一個整數。 每一次調用 wait 操作將會使 semaphore 值減一,而如果 semaphore 值已經為 0,則 wait 操作將會阻塞。 每一次調用post操作將會使semaphore值加一。

需要的頭文件:semaphore.h

信號量標識符:sem_t

主要函數:

sem_init

功能: 用於創建一個信號量,並初始化信號量的值。

函數原型: int sem_init (sem_t* sem, int pshared, unsigned int value);

函數傳入值: sem:信號量。

pshared:決定信號量能否在幾個進程間共享。 由於目前 Linux 還沒有實現進程間共享信息量,所以這個值只能取0。 value:初始計算器

函數返回值: 0:成功;-1:失敗。

其他函數

//等待信號量
int sem_wait (sem_t* sem);
int sem_trywait (sem_t* sem);
//發送信號量
int sem_post (sem_t* sem);
//得到信號量值
int sem_getvalue (sem_t* sem);
//刪除信號量
int sem_destroy (sem_t* sem);

  

功能:sem_wait和sem_trywait相當於 P 操作,它們都能將信號量的值減一, 兩者的區別在於若信號量的值小於零時,sem_wait 將會阻塞進程,而 sem_trywait 則會立即返回。

sem_post 相當於 V 操作,它將信號量的值加一,同時發出喚醒的信號給等待的進程(或線程)。

sem_getvalue 得到信號量的值。

sem_destroy 摧毀信號量。

使用形式:

sem_t sem;
sem_init(&sem, 0, 1); /*信號量初始化*/
...
sem_wait(&sem);   /*等待信號量*/
... /*臨界資源*/
sem_post(&sem);   /*釋放信號量*/

  

信號量與線程鎖、條件變量

信號量與線程鎖、條件變量相比還有以下幾點不同:

  1. 鎖必須是同一個線程獲取以及釋放,否則會死鎖。而條件變量和信號量則不必。
  2. 信號的遞增與減少會被系統自動記住,系統內部有一個計數器實現信號量,不必擔心會丟失,而喚醒一個條件變量時,如果沒有相應的線程在等待該條件變量,這次喚醒將被丟失。

簡單的多線程程序

首先在主函數中,我們使用到了兩個函數,pthread_createpthread_join,並聲明了一個 pthread_t型的變量。

pthread_t 在頭文件 pthread.h 中已經聲明,是線程的標示符

函數 pthread_create 用來創建一個線程,函數原型:

#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
  void *(*start_routine) (void *), void *arg);

  

  • 第一個參數為指向線程標識符的指針,
  • 第二個參數用來設置線程屬性,
  • 第三個參數是線程運行函數的起始地址,
  • 最後一個參數是運行函數的參數。

若我們的函數thread不需要參數,所以最後一個參數設為空指針。第二個參數我們也設為空指針,這樣將生成默認屬性的線程。

返回值:

  • 0: 當創建線程成功時
  • non-zero: 說明創建線程失敗,常見的錯誤返回代碼為
    • EAGAIN 表示系統限制創建新的線程,例如線程數目過多了;
    • EINVAL 表示第二個參數代表的線程屬性值非法。

創建線程成功後,新創建的線程則運行參數三和參數四確定的函數, 原來的線程則繼續運行下一行代碼。

函數pthread_join用來等待一個線程的結束。函數原型為:

#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);

  

第一個參數為被等待的線程標識符,第二個參數為一個用戶定義的指針,它可以用來存儲被等待線程的返回值。這個函數是一個線程阻塞的函數,調用它的函數將一直等待到被等待的線程結束為止,當函數返回時,被等待線程的資源被收回。

一個線程的結束有兩種途徑,

  • 一種是象我們上面的例子一樣,函數結束了,調用它的線程也就結束了;
  • 另一種方式是通過函數 pthread_exit 來實現。它的函數原型為:
#include <pthread.h>

void pthread_exit(void *retval);

  

唯一的參數是函數的返回代碼,只要 pthread_join 中的第二個參數 retval 不是NULL,這個值將被傳遞給 retval。

最後要說明的是,一個線程不能被多個線程等待,否則第一個接收到信號的線程成功返回,其余調用 pthread_join 的線程則返回錯誤代碼 ESRCH

修改線程的屬性

設置線程綁定狀態的函數為 pthread_attr_setscope,它有兩個參數,第一個是指向屬性結構的指針,第二個是綁定類型,它有兩個取值:

  • PTHREAD_SCOPE_SYSTEM(綁定的)
  • PTHREAD_SCOPE_PROCESS(非綁定的)

下面的代碼即創建了一個綁定的線程。

#include <pthread.h>

pthread_attr_t attr;
pthread_t tid;

/*初始化屬性值,均設為默認值*/
pthread_attr_init(&attr);
pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);

pthread_create(&tid, &attr, (void *)task_func, NULL);

  

線程的數據處理

和進程相比,線程的最大優點之一是數據的共享性,各個進程共享父進程處沿襲的數據段,可以方便的獲得、修改數據。

但這也給多線程編程帶來了許多問題。我們必須當心有多個不同的進程訪問相同的變量。 許多函數是不可重入的,即同時不能運行一個函數的多個拷貝(除非使用不同的數據段)。 在函數中聲明的靜態變量常常帶來問題,函數的返回值也會有問題。 因為如果返回的是函數內部靜態聲明的空間的地址,則在一個線程調用該函數得到地址後使用該地址指向的數據時,別的線程可能調用此函數並修改了這一段數據。 在進程中共享的變量必須用關鍵字 volatile 來定義,這是為了防止編譯器在優化時(如 gcc 中使用 -OX 參數)改變它們的使用方式。 為了保護變量,我們必須使用信號量、互斥等方法來保證我們對變量的正確使用。

互斥鎖

互斥鎖用來保證一段時間內只有一個線程在執行一段代碼。必要性顯而易見:假設各個線程向同一個文件順序寫入數據,最後得到的結果一定是災難性的

信號量

原來總是用互斥鎖(MUTEX)和環境變量(cond)去控制線程的通信,用起來挺麻煩的,用信號量(SEM)來通信控制就方便多了!

用到信號量就要包含semaphore.h頭文件。
可以用sem_t類型來聲明一個型號量。

#include <semaphore.h>
sem_t * sem_open(const char *name, int oflag, ...);

用 int sem_init(sem_t *sem, int pshared, unsigned int value) 函數來初始化型號量, 第一個參數就是用 sem_t 聲明的信號量, 第二變量如果為 0,表示這個信號量只是當前進程中的型號量,如果不為 0,這個信號量可能可以在兩個進程中共享。 第三個參數就是初始化信號量的多少值。

  • sem_wait(sem_t *sem) 函數用於接受信號,當 sem > 0 時就能接受到信號,然後將 sem–;
  • sem_post(sem_t *sem) 函數可以增加信號量。
  • sem_destroy(sem_t *sem) 函數用於解除信號量。

以下是一個用信號控制的一個簡單的例子:

#include <stdio.h>
#include <semaphore.h>
#include <pthread.h>

sem_t sem1, sem2;

void *thread1(void *arg) {
    sem_wait(&sem1);
    setbuf(stdout,NULL);//這裏必須註意,由於下面輸出"hello"中沒有‘n’符,所以可能由於輸出緩存已滿,造成輸不出東西來,所以用這個函數把輸出緩存清空
    printf("hello ");
    sem_post(&sem2);
}

void *thread2(void *arg) {
    sem_wait(&sem2);
    printf("world!n");
}

int main() {
    pthread_t t1, t2;

    sem_init(&sem1,0,1);//初始化化信號量為1,所以會先打印線程1
    sem_init(&sem2,0,0);//初始化信號量為0

    pthread_create(&t1,NULL,thread1,NULL);
    pthread_create(&t2,NULL,thread2,NULL);

    pthread_join(t1,NULL);
    pthread_join(t2,NULL);

    sem_destroy(&sem1);
    sem_destroy(&sem2);

    return 0;
}

  

//程序的實現是控制先讓thread1線程打印"hello "再讓thread2線程打印"world!" 

mutex互斥體只用於保護臨界區的代碼(訪問共享資源),而不用於鎖之間的同步,即一個線程釋放mutex鎖後,馬上又可能獲取同一個鎖,而不管其它正在等待該mutex鎖的其它線程。

semaphore信號量除了起到保護臨界區的作用外,還用於鎖同步的功能,即一個線程釋放semaphore後,會保證正在等待該semaphore的線程優先執行,而不會馬上在獲取同一個semaphore。

如果兩個線程想通過一個鎖達到輸出1,2,1,2,1,2這樣的序列,應使用semaphore, 而使用 mutex 的結果可能為1,1,1,1,1,2,2,2,111…..。

Linux/Unix 多線程通信