Linux 線程同步

來源:互聯網
上載者:User

線程



的同步, 發生在多個線程共用相同記憶體的時候, 這時要保證每個線程在每個時刻看到的共用資料是一致的.
如果每個線程使用的變數都是其他線程不會使用的(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, 出錯則返回錯誤編號.

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

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.