詳解linux多執行緒——互斥鎖、條件變數、讀寫鎖、自旋鎖、訊號量
一、互斥鎖(同步)
在多工作業系統中,同時執行的多個任務可能都需要使用同一種資源。這個過程有點類似於,公司部門裡,我在使用著印表機列印東西的同時(還沒有列印完),別人剛好也在此刻使用印表機列印東西,如果不做任何處理的話,打印出來的東西肯定是錯亂的。
線上程裡也有這麼一把鎖——互斥鎖(mutex),互斥鎖是一種簡單的加鎖的方法來控制對共享資源的訪問,互斥鎖只有兩種狀態,即上鎖( lock )和解鎖( unlock )。
【互斥鎖的特點】:
1. 原子性:把一個互斥量鎖定為一個原子操作,這意味著作業系統(或pthread函式庫)保證瞭如果一個執行緒鎖定了一個互斥量,沒有其他執行緒在同一時間可以成功鎖定這個互斥量;
2. 唯一性:如果一個執行緒鎖定了一個互斥量,在它解除鎖定之前,沒有其他執行緒可以鎖定這個互斥量;
3. 非繁忙等待:如果一個執行緒已經鎖定了一個互斥量,第二個執行緒又試圖去鎖定這個互斥量,則第二個執行緒將被掛起(不佔用任何cpu資源),直到第一個執行緒解除對這個互斥量的鎖定為止,第二個執行緒則被喚醒並繼續執行,同時鎖定這個互斥量。
【互斥鎖的操作流程如下】:
1. 在訪問共享資源後臨界區域前,對互斥鎖進行加鎖;
2. 在訪問完成後釋放互斥鎖導上的鎖。在訪問完成後釋放互斥鎖導上的鎖;
3. 對互斥鎖進行加鎖後,任何其他試圖再次對互斥鎖加鎖的執行緒將會被阻塞,直到鎖被釋放。對互斥鎖進行加鎖後,任何其他試圖再次對互斥鎖加鎖的執行緒將會被阻塞,直到鎖被釋放。
#include <pthread.h> #include <time.h> // 初始化一個互斥鎖。 int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); // 對互斥鎖上鎖,若互斥鎖已經上鎖,則呼叫者一直阻塞, // 直到互斥鎖解鎖後再上鎖。 int pthread_mutex_lock(pthread_mutex_t *mutex); // 呼叫該函式時,若互斥鎖未加鎖,則上鎖,返回 0; // 若互斥鎖已加鎖,則函式直接返回失敗,即 EBUSY。int pthread_mutex_trylock(pthread_mutex_t *mutex); // 當執行緒試圖獲取一個已加鎖的互斥量時,pthread_mutex_timedlock 互斥量 // 原語允許繫結執行緒阻塞時間。即非阻塞加鎖互斥量。 int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout); // 對指定的互斥鎖解鎖。 int pthread_mutex_unlock(pthread_mutex_t *mutex); // 銷燬指定的一個互斥鎖。互斥鎖在使用完畢後, // 必須要對互斥鎖進行銷燬,以釋放資源。 int pthread_mutex_destroy(pthread_mutex_t *mutex);
【Demo】(阻塞模式):
//使用互斥量解決多執行緒搶佔資源的問題 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> #include <string.h> char* buf[5]; //字元指標陣列 全域性變數 int pos; //用於指定上面陣列的下標 //1.定義互斥量 pthread_mutex_t mutex; void *task(void *p) { //3.使用互斥量進行加鎖 pthread_mutex_lock(&mutex); buf[pos] = (char *)p; sleep(1); pos++; //4.使用互斥量進行解鎖 pthread_mutex_unlock(&mutex); } int main(void) { //2.初始化互斥量, 預設屬性 pthread_mutex_init(&mutex, NULL); //1.啟動一個執行緒 向陣列中儲存內容 pthread_t tid, tid2; pthread_create(&tid, NULL, task, (void *)"zhangfei"); pthread_create(&tid2, NULL, task, (void *)"guanyu"); //2.主執行緒程序等待,並且列印最終的結果 pthread_join(tid, NULL); pthread_join(tid2, NULL); //5.銷燬互斥量 pthread_mutex_destroy(&mutex); int i = 0; printf("字元指標陣列中的內容是:"); for(i = 0; i < pos; ++i) { printf("%s ", buf[i]); } printf("\n"); return 0; }
【Demo】(非阻塞模式):
#include <stdio.h> #include <pthread.h> #include <time.h> #include <string.h> int main (void) { int err; struct timespec tout; struct tm *tmp; char buf[64]; pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_lock (&lock); printf ("mutex is locked\n"); clock_gettime (CLOCK_REALTIME, &tout); tmp = localtime (&tout.tv_sec); strftime (buf, sizeof (buf), "%r", tmp); printf ("current time is %s\n", buf); tout.tv_sec += 10; err = pthread_mutex_timedlock (&lock, &tout); clock_gettime (CLOCK_REALTIME, &tout); tmp = localtime (&tout.tv_sec); strftime (buf, sizeof (buf), "%r", tmp); printf ("the time is now %s\n", buf); if (err == 0) printf ("mutex locked again\n"); else printf ("can`t lock mutex again:%s\n", strerror (err)); return 0; }
二、條件變數(同步)
與互斥鎖不同,條件變數是用來等待而不是用來上鎖的。條件變數用來自動阻塞一個執行緒,直 到某特殊情況發生為止。通常條件變數和互斥鎖同時使用。
條件變數使我們可以睡眠等待某種條件出現。條件變數是利用執行緒間共享的全域性變數進行同步 的一種機制,主要包括兩個動作:
一個執行緒等待"條件變數的條件成立"而掛起;
另一個執行緒使 “條件成立”(給出條件成立訊號)。
【原理】:
條件的檢測是在互斥鎖的保護下進行的。執行緒在改變條件狀態之前必須首先鎖住互斥量。如果一個條件為假,一個執行緒自動阻塞,並釋放等待狀態改變的互斥鎖。如果另一個執行緒改變了條件,它發訊號給關聯的條件變數,喚醒一個或多個等待它的執行緒,重新獲得互斥鎖,重新評價條件。如果兩程序共享可讀寫的記憶體,條件變數 可以被用來實現這兩程序間的執行緒同步。
【條件變數的操作流程如下】:
1. 初始化:init()或者pthread_cond_tcond=PTHREAD_COND_INITIALIER;屬性置為NULL;
2. 等待條件成立:pthread_wait,pthread_timewait.wait()釋放鎖,並阻塞等待條件變數為真 timewait()設定等待時間,仍未signal,返回ETIMEOUT(加鎖保證只有一個執行緒wait);
3. 啟用條件變數:pthread_cond_signal,pthread_cond_broadcast(啟用所有等待執行緒)
4. 清除條件變數:destroy;無執行緒等待,否則返回EBUSY清除條件變數:destroy;無執行緒等待,否則返回EBUSY
#include <pthread.h> // 初始化條件變數 int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr); // 阻塞等待 int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex); // 超時等待 int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex, const timespec *abstime); // 解除所有執行緒的阻塞 int pthread_cond_destroy(pthread_cond_t *cond); // 至少喚醒一個等待該條件的執行緒 int pthread_cond_signal(pthread_cond_t *cond); // 喚醒等待該條件的所有執行緒 int pthread_cond_broadcast(pthread_cond_t *cond);
1、執行緒的條件變數例項1
Jack開著一輛計程車來到一個站點停車,看見沒人就走了。過段時間,Susan來到站點準備乘車,但是沒有來,於是就等著。過了一會Mike開著車來到了這個站點,Sunsan就上了Mike的車走了。如圖所示:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> pthread_cond_t taxicond = PTHREAD_COND_INITIALIZER; pthread_mutex_t taximutex = PTHREAD_MUTEX_INITIALIZER; void *traveler_arrive(void *name) { char *p = (char *)name; printf ("Travelr: %s need a taxi now!\n", p); // 加鎖,把訊號量加入佇列,釋放訊號量 pthread_mutex_lock(&taximutex); pthread_cond_wait(&taxicond, &taximutex); pthread_mutex_unlock(&taximutex); printf ("traveler: %s now got a taxi!\n", p); pthread_exit(NULL); } void *taxi_arrive(void *name) { char *p = (char *)name; printf ("Taxi: %s arrives.\n", p); // 給執行緒或者條件發訊號,一定要在改變條件狀態後再給執行緒發訊號 pthread_cond_signal(&taxicond); pthread_exit(NULL); } int main (int argc, char **argv) { char *name; pthread_t thread; pthread_attr_t threadattr; // 執行緒屬性 pthread_attr_init(&threadattr); // 執行緒屬性初始化 // 建立三個執行緒 name = "Jack"; pthread_create(&thread, &threadattr, taxi_arrive, (void *)name); sleep(1); name = "Susan"; pthread_create(&thread, &threadattr, traveler_arrive, (void *)name); sleep(1); name = "Mike"; pthread_create(&thread, &threadattr, taxi_arrive, (void *)name); sleep(1); return 0; }
2、執行緒的條件變數例項2
Jack開著一輛計程車來到一個站點停車,看見沒人就等著。過段時間,Susan來到站點準備乘車看見了Jack的計程車,於是就上去了。過了一會Mike開著車來到了這個站點,看見沒人救等著。如圖所示:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> int travelercount = 0; pthread_cond_t taxicond = PTHREAD_COND_INITIALIZER; pthread_mutex_t taximutex = PTHREAD_MUTEX_INITIALIZER; void *traveler_arrive(void *name) { char *p = (char *)name; pthread_mutex_lock(&taximutex); printf ("traveler: %s need a taxi now!\n", p); travelercount++; pthread_cond_wait(&taxicond, &taximutex); pthread_mutex_unlock(&taximutex); printf ("traveler: %s now got a taxi!\n", p); pthread_exit(NULL); } void *taxi_arrive(void *name) { char *p = (char *)name; printf ("Taxi: %s arrives.\n", p); for(;;) { if(travelercount) { pthread_cond_signal(&taxicond); travelercount--; break; } } pthread_exit(NULL); } int main (int argc, char **argv) { char *name; pthread_t thread; pthread_attr_t threadattr; pthread_attr_init(&threadattr); name = "Jack"; pthread_create(&thread, &threadattr, taxi_arrive, name); sleep(1); name = "Susan"; pthread_create(&thread, &threadattr, traveler_arrive, name); sleep(3); name = "Mike"; pthread_create(&thread, &threadattr, taxi_arrive, name); sleep(4); return 0; }
3、虛假喚醒(spurious wakeup)
虛假喚醒(spurious wakeup)在採用條件等待時:
while(條件不滿足) { condition_wait(cond, mutex); } // 而不是: If( 條件不滿足 ) { Condition_wait(cond,mutex); }
這是因為可能會存在虛假喚醒”spurious wakeup”的情況。
也就是說,即使沒有執行緒呼叫condition_signal, 原先呼叫condition_wait的函式也可能會返回。此時執行緒被喚醒了,但是條件並不滿足,這個時候如果不對條件進行檢查而往下執行,就可能會導致後續的處理出現錯誤。
虛假喚醒在linux的多處理器系統中/在程式接收到訊號時可能回發生。在Windows系統和JAVA虛擬機器上也存在。在系統設計時應該可以避免虛假喚醒,但是這會影響條件變數的執行效率,而既然通過while迴圈就能避免虛假喚醒造成的錯誤,因此程式的邏輯就變成了while迴圈的情況。
四、讀寫鎖(同步)
讀寫鎖與互斥量類似,不過讀寫鎖允許更改的並行性,也叫共享互斥鎖。互斥量要麼是鎖住狀態,要麼就是不加鎖狀態,而且一次只有一個執行緒可以對其加鎖。讀寫鎖可以有3種狀態:讀模式下加鎖狀態、寫模式加鎖狀態、不加鎖狀態。
一次只有一個執行緒可以佔有寫模式的讀寫鎖,但是多個執行緒可以同時佔有讀模式的讀寫鎖(允許多個執行緒讀但只允許一個執行緒寫)。
【讀寫鎖的特點】:
如果有其它執行緒讀資料,則允許其它執行緒執行讀操作,但不允許寫操作;
如果有其它執行緒寫資料,則其它執行緒都不允許讀、寫操作。
【讀寫鎖的規則】:
如果某執行緒申請了讀鎖,其它執行緒可以再申請讀鎖,但不能申請寫鎖;
如果某執行緒申請了寫鎖,其它執行緒不能申請讀鎖,也不能申請寫鎖。
讀寫鎖適合於對資料結構的讀次數比寫次數多得多的情況。
#include <pthread.h> // 初始化讀寫鎖 int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr); // 申請讀鎖 int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock ); // 申請寫鎖 int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock ); // 嘗試以非阻塞的方式來在讀寫鎖上獲取寫鎖, // 如果有任何的讀者或寫者持有該鎖,則立即失敗返回。 int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); // 解鎖 int pthread_rwlock_unlock (pthread_rwlock_t *rwlock); // 銷燬讀寫鎖 int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
【Demo】:
// 一個使用讀寫鎖來實現 4 個執行緒讀寫一段資料是例項。
// 在此示例程式中,共建立了 4 個執行緒,
// 其中兩個執行緒用來寫入資料,兩個執行緒用來讀取資料
#include <stdio.h> #include <unistd.h> #include <pthread.h> pthread_rwlock_t rwlock; //讀寫鎖 int num = 1; //讀操作,其他執行緒允許讀操作,卻不允許寫操作 void *fun1(void *arg) { while(1) { pthread_rwlock_rdlock(&rwlock); printf("read num first == %d\n", num); pthread_rwlock_unlock(&rwlock); sleep(1); } } //讀操作,其他執行緒允許讀操作,卻不允許寫操作 void *fun2(void *arg) { while(1) { pthread_rwlock_rdlock(&rwlock); printf("read num second == %d\n", num); pthread_rwlock_unlock(&rwlock); sleep(2); } } //寫操作,其它執行緒都不允許讀或寫操作 void *fun3(void *arg) { while(1) { pthread_rwlock_wrlock(&rwlock); num++; printf("write thread first\n"); pthread_rwlock_unlock(&rwlock); sleep(2); } } //寫操作,其它執行緒都不允許讀或寫操作 void *fun4(void *arg) { while(1) { pthread_rwlock_wrlock(&rwlock); num++; printf("write thread second\n"); pthread_rwlock_unlock(&rwlock); sleep(1); } } int main() { pthread_t ptd1, ptd2, ptd3, ptd4; pthread_rwlock_init(&rwlock, NULL);//初始化一個讀寫鎖 //建立執行緒 pthread_create(&ptd1, NULL, fun1, NULL); pthread_create(&ptd2, NULL, fun2, NULL); pthread_create(&ptd3, NULL, fun3, NULL); pthread_create(&ptd4, NULL, fun4, NULL); //等待執行緒結束,回收其資源 pthread_join(ptd1, NULL); pthread_join(ptd2, NULL); pthread_join(ptd3, NULL); pthread_join(ptd4, NULL); pthread_rwlock_destroy(&rwlock);//銷燬讀寫鎖 return 0; }
五、自旋鎖(同步)
自旋鎖與互斥量功能一樣,唯一一點不同的就是互斥量阻塞後休眠讓出cpu,而自旋鎖阻塞後不會讓出cpu,會一直忙等待,直到得到鎖。
自旋鎖在使用者態使用的比較少,在核心使用的比較多!自旋鎖的使用場景:鎖的持有時間比較短,或者說小於2次上下文切換的時間。
自旋鎖在使用者態的函式介面和互斥量一樣,把pthread_mutex_xxx()中mutex換成spin,如:pthread_spin_init()。
六、訊號量(同步與互斥)
訊號量廣泛用於程序或執行緒間的同步和互斥,訊號量本質上是一個非負的整數計數器,它被用來控制對公共資源的訪問。
程式設計時可根據操作訊號量值的結果判斷是否對公共資源具有訪問的許可權,當訊號量值大於 0 時,則可以訪問,否則將阻塞。PV 原語是對訊號量的操作,一次 P 操作使訊號量減1,一次 V 操作使訊號量加1。
#include <semaphore.h> // 初始化訊號量 int sem_init(sem_t *sem, int pshared, unsigned int value); // 訊號量 P 操作(減 1) int sem_wait(sem_t *sem); // 以非阻塞的方式來對訊號量進行減 1 操作 int sem_trywait(sem_t *sem); // 訊號量 V 操作(加 1) int sem_post(sem_t *sem); // 獲取訊號量的值 int sem_getvalue(sem_t *sem, int *sval); // 銷燬訊號量 int sem_destroy(sem_t *sem);
【訊號量用於同步】:
// 訊號量用於同步例項
#include <stdio.h> #include <unistd.h> #include <pthread.h> #include <semaphore.h> sem_t sem_g,sem_p; //定義兩個訊號量 char ch = 'a'; void *pthread_g(void *arg) //此執行緒改變字元ch的值 { while(1) { sem_wait(&sem_g); ch++; sleep(1); sem_post(&sem_p); } } void *pthread_p(void *arg) //此執行緒列印ch的值 { while(1) { sem_wait(&sem_p); printf("%c",ch); fflush(stdout); sem_post(&sem_g); } } int main(int argc, char *argv[]) { pthread_t tid1,tid2; sem_init(&sem_g, 0, 0); // 初始化訊號量為0 sem_init(&sem_p, 0, 1); // 初始化訊號量為1 // 建立兩個執行緒 pthread_create(&tid1, NULL, pthread_g, NULL); pthread_create(&tid2, NULL, pthread_p, NULL); // 回收執行緒 pthread_join(tid1, NULL); pthread_join(tid2, NULL); return 0; }
【訊號量用於互斥】:
// 訊號量用於互斥例項 #include <stdio.h> #include <pthread.h> #include <unistd.h> #include <semaphore.h> sem_t sem; //訊號量 void printer(char *str) { sem_wait(&sem);//減一,p操作 while(*str) // 輸出字串(如果不用互斥,此處可能會被其他執行緒入侵) { putchar(*str); fflush(stdout); str++; sleep(1); } printf("\n"); sem_post(&sem);//加一,v操作 } void *thread_fun1(void *arg) { char *str1 = "hello"; printer(str1); } void *thread_fun2(void *arg) { char *str2 = "world"; printer(str2); } int main(void) { pthread_t tid1, tid2; sem_init(&sem, 0, 1); //初始化訊號量,初始值為 1 //建立 2 個執行緒 pthread_create(&tid1, NULL, thread_fun1, NULL); pthread_create(&tid2, NULL, thread_fun2, NULL); //等待執行緒結束,回收其資源 pthread_join(tid1, NULL); pthread_join(tid2, NULL); sem_destroy(&sem); //銷燬訊號量 return 0; }