Linux 線程同步

線程 的同步, 發生在多個線程共享相同內存的時候, 這時要保證每個線程在每個時刻看到的共享數據是一致的. 如果每個線程使用的變量都是其他線程不會使用的(read & write), 或者變量是隻讀的, 就不存在一致性問題. 但是, 如果兩個或兩個以上的線程可以read/write一個變量時, 就需要對線程進行同步, 以確保它們在訪問該變量時, 不會得到無效的值, 同時也可以唯一地修改該變量並使它生效.

    以上就是我們所說的線程同步.

    線程同步有三種常用的機制: 互斥量(mutex), 讀寫鎖(rwlock)和條件變量(cond).

    互斥量有兩種狀態: lock和unlock, 它確保同一時間只有一個線程訪問數據;

    讀寫鎖有三種狀態: 讀加鎖, 寫加鎖, 不加鎖, 只有一個線程可以佔有寫模式的讀寫鎖, 但是可以有多個線程同時佔有讀模式的讀寫鎖.

    條件變量則給多個線程提供了一個會合的場所, 與互斥量一起使用時, 允許線程以無競爭的方式等待特定條件的發生.

互斥量

    互斥量從本質上說就是一把鎖, 提供對共享資源的保護訪問.

1. 初始化:

    在 Linux 下, 線程的互斥量數據類型是pthread_mutex_t. 在使用前, 要對它進行初始化:

對於靜態分配的互斥量, 可以把它設置爲PTHREAD_MUTEX_INITIALIZER, 或者調用pthread_mutex_init.

對於動態分配的互斥量, 在申請內存(malloc)之後, 通過pthread_mutex_init進行初始化, 並且在釋放內存(free)前需要調用pthread_mutex_destroy.

  • 原型:
    • int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restric attr);
    • int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • 頭文件: <pthread.h>
  • 返回值: 成功則返回0, 出錯則返回錯誤編號.
  • 說明: 如果使用默認的屬性初始化互斥量, 只需把attr設爲NULL. 其他值在以後講解.

2. 互斥操作:

    對共享資源的訪問, 要對互斥量進行加鎖, 如果互斥量已經上了鎖, 調用線程會阻塞, 直到互斥量被解鎖. 在完成了對共享資源的訪問後, 要對互斥量進行解鎖.

首先說一下加鎖函數:

  • 頭文件: <pthread.h>
  • 原型:
    • int pthread_mutex_lock(pthread_mutex_t *mutex);
    • int pthread_mutex_trylock(pthread_mutex_t *mutex);
  • 返回值: 成功則返回0, 出錯則返回錯誤編號.
  • 說 明: 具體說一下trylock函數, 這個函數是非阻塞調用模式, 也就是說, 如果互斥量沒被鎖住, trylock函數將把互斥量加鎖, 並獲得對共享資源的訪問權限; 如果互斥量被鎖住了, trylock函數將不會阻塞等待而直接返回EBUSY, 表示共享資源處於忙狀態.

再說一下解所函數:

  • 頭文件: <pthread.h>
  • 原型: int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • 返回值: 成功則返回0, 出錯則返回錯誤編號.

3. 死鎖:

    死鎖主要發生在有多個依賴鎖存在時, 會在一個線程試圖以與另一個線程相反順序鎖住互斥量時發生. 如何避免死鎖是使用互斥量應該格外注意的東西.

    總體來講, 有幾個不成文的基本原則:

  • 對共享資源操作前一定要獲得鎖.
  • 完成操作以後一定要釋放鎖.
  • 儘量短時間地佔用鎖.
  • 如果有多鎖, 如獲得順序是ABC連環扣, 釋放順序也應該是ABC.
  • 線程錯誤返回時應該釋放它所獲得的鎖.

信號量
  信號量本質上是一個非負的整數計數器,它被用來控制對公共資源的訪問。當公共資源增加時,調用函數sem_post()增加 信號量。只有當信號量值大於0時,才能使用公共資源,使用後,函數sem_wait()減少信號量。函數sem_trywait()和函數 pthread_ mutex_trylock()起同樣的作用,它是函數sem_wait()的非阻塞版本。下面我們逐個介紹和信號量有關的一些函數,它們都在頭文件 /usr/include/semaphore.h中定義。
  信號量的數據類型爲結構sem_t,它本質上是一個長整型的數。函數sem_init()用來初始化一個信號量。它的原型爲:
  extern int sem_init __P ((sem_t *__sem, int __pshared, unsigned int __value));
  sem爲指向信號量結構的一個指針;pshared不爲0時此信號量在進程間共享,否則只能爲當前進程的所有線程共享;value給出了信號量的初始值。

  函數sem_post( sem_t *sem )用來增加信號量的值。當有線程阻塞在這個信號量上時,調用這個函數會使其中的一個線程不在阻塞,選擇機制同樣是由線程的調度策略決定的。
  函數sem_wait( sem_t *sem )被用來阻塞當前線程直到信號量sem的值大於0,解除阻塞後將sem的值減一,表明公共資源經使用後減少。函數sem_trywait ( sem_t *sem )是函數sem_wait()的非阻塞版本,它直接將信號量sem的值減一。
  函數sem_destroy(sem_t *sem)用來釋放信號量sem。
  下面我們來看一個使用信號量的例子。在這個例子中,一共有4個線程,其中兩個線程負責從文件讀取數據到公共的緩衝區,另兩個線程從緩衝區讀取數據作不同的處理(加和乘運算)。
/* File sem.c */
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#define MAXSTACK 100
int stack[MAXSTACK][2];
int size=0;
sem_t sem;
/* 從文件1.dat讀取數據,每讀一次,信號量加一*/
void ReadData1(void){
FILE *fp=fopen("1.dat","r");
while(!feof(fp)){
fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
sem_post(&sem);
++size;
}
fclose(fp);
}

/*從文件2.dat讀取數據*/
void ReadData2(void){
FILE *fp=fopen("2.dat","r");
while(!feof(fp)){
fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
sem_post(&sem);
++size;
}
fclose(fp);
}
/*阻塞等待緩衝區有數據,讀取數據後,釋放空間,繼續等待*/
void HandleData1(void){
while(1){
sem_wait(&sem);
printf("Plus:%d+%d=%d/n",stack[size][0],stack[size][1],
stack[size][0]+stack[size][1]);
--size;
}
}

void HandleData2(void){
while(1){
sem_wait(&sem);
printf("Multiply:%d*%d=%d/n",stack[size][0],stack[size][1],
stack[size][0]*stack[size][1]);
--size;
}
}

int main(void){
pthread_t t1,t2,t3,t4;
sem_init(&sem,0,0);
pthread_create(&t1,NULL,(void *)HandleData1,NULL);
pthread_create(&t2,NULL,(void *)HandleData2,NULL);
pthread_create(&t3,NULL,(void *)ReadData1,NULL);
pthread_create(&t4,NULL,(void *)ReadData2,NULL);
/* 防止程序過早退出,讓它在此無限期等待*/
pthread_join(t1,NULL);
}

  在Linux下,我們用命令gcc -lpthread sem.c -o sem生成可執行文件sem。 我們事先編輯好數據文件1.dat和2.dat,假設它們的內容分別爲1 2 3 4 5 6 7 8 9 10和 -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 ,我們運行sem,得到如下的結果:
Multiply:-1*-2=2
Plus:-1+-2=-3
Multiply:9*10=90
Plus:-9+-10=-19
Multiply:-7*-8=56
Plus:-5+-6=-11
Multiply:-3*-4=12
Plus:9+10=19
Plus:7+8=15
Plus:5+6=11


  從中我們可以看出各個線程間的競爭關係。而數值並未按我們原先的順序顯示出來這是由於size這個數值被各個線程任意修改的緣故。這也往往是多線程編程要注意的問題。

寫鎖

    在線程同步系列的第一篇文章裏已經說過, 讀寫鎖是因爲有3種狀態, 所以可以有更高的並行性.

1. 特性:

    一次只有一個線程可以佔有寫模式的讀寫鎖, 但是可以有多個線程同時佔有讀模式的讀寫鎖. 正是因爲這個特性,

  • 當讀寫鎖是寫加鎖狀態時, 在這個鎖被解鎖之前, 所有試圖對這個鎖加鎖的線程都會被阻塞.
  • 當讀寫鎖在讀加鎖狀態時, 所有試圖以讀模式對它進行加鎖的線程都可以得到訪問權, 但是如果線程希望以寫模式對此鎖進行加鎖, 它必須阻塞知道所有的線程釋放鎖.
  • 通常, 當讀寫鎖處於讀模式鎖住狀態時, 如果有另外線程試圖以寫模式加鎖, 讀寫鎖通常會阻塞隨後的讀模式鎖請求, 這樣可以避免讀模式鎖長期佔用, 而等待的寫模式鎖請求長期阻塞.

2. 適用性:

    讀寫鎖適合於對數據結構的讀次數比寫次數多得多的情況. 因爲, 讀模式鎖定時可以共享, 以寫模式鎖住時意味着獨佔, 所以讀寫鎖又叫共享-獨佔鎖.

3. 初始化和銷燬:

#include < pthread.h >

int pthread_rwlock_init(pthread_rwlock_t * restrict rwlock, const pthread_rwlockattr_t * restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *
rwlock);

成功則返回0, 出錯則返回錯誤編號.

同互斥量以上, 在釋放讀寫鎖佔用的內存之前, 需要先通過pthread_rwlock_destroy對讀寫鎖進行清理工作, 釋放由init分配的資源.

 

4. 讀和寫:

#include < pthread.h >

int pthread_rwlock_rdlock(pthread_rwlock_t * rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *
rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *
rwlock);

成功則返回0, 出錯則返回錯誤編號.
這3個函數分別實現獲取讀鎖, 獲取寫鎖和釋放鎖的操作. 獲取鎖的兩個函數是阻塞操作, 同樣, 非阻塞的函數爲:
#include < pthread.h >

int pthread_rwlock_tryrdlock(pthread_rwlock_t * rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *
rwlock);

成功則返回0, 出錯則返回錯誤編號.
非阻塞的獲取鎖操作, 如果可以獲取則返回0, 否則返回錯誤的EBUSY.


條件變量


     條件變量分爲兩部分: 條件和變量. 條件本身是由互斥量保護的. 線程在改變條件狀態前先要鎖住互斥量.

1. 初始化:

    條件變量採用的數據類型是pthread_cond_t, 在使用之前必須要進行初始化, 這包括兩種方式:

  • 靜態: 可以把常量PTHREAD_COND_INITIALIZER給靜態分配的條件變量.
  • 動態: pthread_cond_init函數, 是釋放動態條件變量的內存空間之前, 要用pthread_cond_destroy對其進行清理.
#include < pthread.h >

int pthread_cond_init(pthread_cond_t * restrict cond, pthread_condattr_t * restrict attr);
int pthread_cond_destroy(pthread_cond_t *
cond);

成功則返回0, 出錯則返回錯誤編號.

    當pthread_cond_init的attr參數爲NULL時, 會創建一個默認屬性的條件變量; 非默認情況以後討論.

 

2. 等待條件:

#include < pthread.h >

int pthread_cond_wait(pthread_cond_t * restrict cond, pthread_mutex_t * restric mutex);
int pthread_cond_timedwait(pthread_cond_t * restrict cond, pthread_mutex_t * restrict mutex, const struct timespec *
restrict timeout);

成功則返回0, 出錯則返回錯誤編號.

    這兩個函數分別是阻塞等待和超時等待.

    等待條件函數等待條件變爲真, 傳遞給pthread_cond_wait的互斥量對條件進行保護, 調用者把鎖住的互斥量傳遞給函數. 函數把調用線程放到等待條件的線程列表上, 然後對互斥量解鎖, 這兩個操作是原子的. 這樣便關閉了條件檢查和線程進入休眠狀態等待條件改變這兩個操作之間的時間通道, 這樣線程就不會錯過條件的任何變化.

    當pthread_cond_wait返回時, 互斥量再次被鎖住.

 

3. 通知條件:

#include < pthread.h >

int pthread_cond_signal(pthread_cond_t * cond);
int pthread_cond_broadcast(pthread_cond_t *
cond);

成功則返回0, 出錯則返回錯誤編號.

    這兩個函數用於通知線程條件已經滿足. 調用這兩個函數, 也稱向線程或條件發送信號. 必須注意, 一定要在改變條件狀態以後再給線程發送信號.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章