【Linux】執行緒安全-同步與互斥
執行緒安全:多個執行緒執行流對臨界資源的不安全爭搶操作
實現:如何讓執行緒之間安全對臨界資源進行操作就是同步與互斥
互斥:同一時間臨界資源的唯一訪問性
mutex(互斥量)
- ⼤部分情況,執行緒使⽤的資料都是區域性變數,變數的地址空間線上程棧空間內,這種情況,變數歸屬單個執行緒,其他執行緒⽆法獲得這種變數。
- 但有時候,很多變數都需要線上程間共享,這樣的變數稱為共享變數,可以通過資料的共享,完成執行緒之間的互動。
- 多個執行緒併發的操作共享變數,會帶來⼀些問題。
我們先來看一個例子:操作共享變數的售票系統。
#include <stdio.h> #include<stdlib.h> #include<unistd.h> #include<pthread.h> #include<string.h> int ticket = 100; void* route(void* arg) { int id = (int)arg; while(1) { if(ticket > 0) { usleep(1000); printf("pthread %d -> %d\n", id, ticket); ticket--; } else break; } return NULL; } int main() { pthread_t tid[4]; int i, ret; for(i = 0; i < 4; ++i) { ret = pthread_create(&tid[i], NULL, route, (void*)i); if(ret != 0) return -1; } for(i = 0; i < 4; ++i) pthread_join(tid[i], NULL); return 0; }
很明顯,ticket都賣完了,還有執行緒在搶票。
- if 語句判斷條件為真以後,程式碼可以併發的切換到其他執行緒
- usleep這個模擬漫⻓業務的過程,在這個漫⻓的業務過程中,可能有很多個執行緒會進⼊該程式碼段
- --ticket操作本⾝就不是⼀個原⼦操作
--操作並不是原⼦操作,⽽是對應三條彙編指令:
- load:將共享變數ticket從記憶體載入到暫存器中
- update: 更新暫存器⾥⾯的值,執⾏-1操作
- store:將新值,從暫存器寫回共享變數ticket的記憶體地址。
要解決以上問題,需要做到三點:
- 程式碼必須要有互斥⾏為:當代碼進⼊臨界區執⾏時,不允許其他執行緒進⼊該臨界區。
- 如果多個執行緒同時要求執⾏臨界區的程式碼,並且臨界區沒有執行緒在執⾏,那麼只能允許⼀個執行緒進⼊該臨界區。
- 如果執行緒不在臨界區中執⾏,那麼該執行緒不能阻⽌其他執行緒進⼊臨界區。
要做到這三點,本質上就是需要⼀把鎖。Linux上提供的這把鎖叫互斥量。
互斥量的介面:
初始化互斥量
初始化互斥量有兩種⽅法:
- ⽅法1,靜態分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
- ⽅法2,動態分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); 引數: mutex:要初始化的互斥量 attr:NULL
銷燬互斥量
銷燬互斥量需要注意:
- 使⽤PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要銷燬
- 不要銷燬⼀個已經加鎖的互斥量
- 已經銷燬的互斥量,要確保後⾯不會有執行緒再嘗試加鎖
int pthread_mutex_destroy(pthread_mutex_t *mutex);
互斥量加鎖和解鎖
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失敗返回錯誤號
調⽤pthread_ lock 時,可能會遇到以下情況:
- 互斥量處於未鎖狀態,該函式會將互斥量鎖定,同時返回成功
- 發起函式調⽤時,其他執行緒已經鎖定互斥量,或者存在其他執行緒同時申請互斥量,但沒有競爭到互斥量,那麼pthread_ lock調⽤會陷⼊阻塞,等待互斥量解鎖。
改進上面的售票系統
#include <stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<string.h>
int ticket = 100;
pthread_mutex_t mutex;
void* route(void* arg)
{
int id = (int)arg;
while(1)
{
pthread_mutex_lock(&mutex);
if(ticket > 0)
{
usleep(1000);
printf("pthread %d -> %d\n", id, ticket);
ticket--;
}
else
{
pthread_mutex_unlock(&mutex);
break;
}
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main()
{
pthread_t tid[4];
int i, ret;
pthread_mutex_init(&mutex, NULL);
for(i = 0; i < 4; ++i)
{
ret = pthread_create(&tid[i], NULL, route, (void*)i);
if(ret != 0)
return -1;
}
for(i = 0; i < 4; ++i)
pthread_join(tid[i], NULL);
pthread_mutex_destroy(&mutex);
return 0;
}
很明顯,加鎖之後執行緒安全得到了保證。至於只有一個執行緒訪問,這與時間片有關。
死鎖:
產生死鎖的原因主要是:
(1) 因為系統資源不足。
(2) 程序執行推進的順序不合適。
(3) 資源分配不當等。
如果系統資源充足,程序的資源請求都能夠得到滿足,死鎖出現的可能性就很低,否則就會因爭奪有限的資源而陷入死鎖。其次,程序執行推進順序與速度不同,也可能產生死鎖。
產生死鎖的四個必要條件:
(1) 互斥條件:一個資源每次只能被一個程序使用。
(2) 請求與保持條件:一個程序因請求資源而阻塞時,對已獲得的資源保持不放。
(3) 不剝奪條件:程序已獲得的資源,在末使用完之前,不能強行剝奪。
(4) 迴圈等待條件:若干程序之間形成一種頭尾相接的迴圈等待資源關係。
這四個條件是死鎖的必要條件,只要系統發生死鎖,這些條件必然成立,而只要上述條件之一不滿足,就不會發生死鎖。
死鎖的解除與預防:
理解了死鎖的原因,尤其是產生死鎖的四個必要條件,就可以最大可能地避免、預防和
解除死鎖。所以,在系統設計、程序排程等方面注意如何不讓這四個必要條件成立,如何確
定資源的合理分配演算法,避免程序永久佔據系統資源。此外,也要防止程序在處於等待狀態
的情況下佔用資源。因此,對資源的分配要給予合理的規劃。
避免死鎖:銀行家演算法,死鎖檢測演算法
同步:對臨界資源操作的時序可控性
條件變數:等待、喚醒
條件變數一共提供了兩個功能,一個是等待,一個是喚醒
對於一個外部條件進行判斷,如果條件滿足則繼續操作;如果條件不滿足怎等待
為了能夠讓程式繼續操作,需要其他執行流修改條件,是滿足條件,並喚醒對方
這裡所說的外部條件,條件變數不會提供是我們使用者設定的判斷依據
條件變數
- 當⼀個執行緒互斥地訪問某個變數時,它可能發現在其它執行緒改變狀態之前,它什麼也做不了。
- 例如⼀個執行緒訪問佇列時,發現佇列為空,它只能等待,只到其它執行緒將⼀個節點新增到佇列中。這種情況就需要⽤到條件變數
條件變數函式
初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
引數:
cond:要初始化的條件變數
attr:NULL
銷燬
int pthread_cond_destroy(pthread_cond_t *cond)
等待條件滿⾜
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
引數:
cond:要在這個條件變數上等待
mutex:互斥量,後⾯詳細解釋
喚醒等待
喚醒所有等待
int pthread_cond_broadcast(pthread_cond_t *cond);
喚醒單個等待
int pthread_cond_signal(pthread_cond_t *cond);
示例:
#include <stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>
pthread_cond_t sale;
pthread_cond_t eat;
pthread_mutex_t mutex;
int have_noodle = 0;
void* sale_noodle(void* arg)
{
int id = (int)arg;
while(1)
{
pthread_mutex_lock(&mutex);
if(have_noodle == 1)
pthread_cond_wait(&sale, &mutex);
printf("pthread %d create noodle!!\n", id);
have_noodle = 1;
//生產出來後通知買方便麵的人
pthread_cond_signal(&eat);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void* eat_noodle(void* arg)
{
while(1)
{
//因為have_noodle的操作也是一個臨界資源的操作,因此需要被
//保護,使用互斥鎖進行保護
pthread_mutex_lock(&mutex);
if (have_noodle == 0)
{
//因為等待時間不確定,因此有可能會浪費很多等待時間
//因此使用環境變數提供的死等操作,但是這個死等需要能夠
// 被喚醒,這樣的話,一旦方便麵生產出來直接喚醒我們的死
//等,不會浪費多餘的等待時間
//防止不滿足條件陷入休眠,沒有解鎖,對方無法獲取鎖,沒
//辦法生產方便麵,因此需要解鎖,
//但是解鎖和休眠必須是原子操作
pthread_cond_wait(&eat, &mutex);
//被喚醒,這時候可以繼續吃麵,並且修改條件,但是條件是
//臨界資源,因此需要加鎖,
// pthread_cond_wait整體操作
// 解鎖-》休眠-》被喚醒後加鎖(但是這不是一個阻塞操作,
//而是直接計數器置0)
}
printf("eat noodle!! good!!\n");
have_noodle = 0;
pthread_mutex_unlock(&mutex);
//吃完之後通知一下賣方便麵的
pthread_cond_signal(&sale);
}
}
int main()
{
pthread_t tid1, tid2;
int ret;
//條件變數初始化
pthread_cond_init(&eat, NULL);
pthread_cond_init(&sale, NULL);
pthread_mutex_init(&mutex, NULL);
ret = pthread_create(&tid1, NULL, sale_noodle, (void*)1);
if(ret != 0)
return -1;
ret = pthread_create(&tid1, NULL, sale_noodle, (void*)2);
if(ret != 0)
return -1;
ret = pthread_create(&tid2, NULL, eat_noodle, NULL);
if(ret != 0)
return -1;
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
//條件變數銷燬
pthread_cond_destroy(&eat);
pthread_cond_destroy(&sale);
pthread_mutex_destroy(&mutex);
return 0;
}
為什麼pthread_ cond_ wait 需要互斥量?
- 條件等待是執行緒間同步的⼀種⼿段,如果只有⼀個執行緒,條件不滿⾜,⼀直等下去都不會滿⾜,所以必須要有⼀個執行緒通過某些操作,改變共享變數,使原先不滿⾜的條件變得滿⾜,並且友好的通知等待在條件變數上的執行緒。
- 條件不會⽆緣⽆故的突然變得滿⾜了,必然會牽扯到共享資料的變化。所以⼀定要⽤互斥鎖來保護。沒有互斥鎖就⽆法安全的獲取和修改共享資料。
按照上⾯的說法,我們設計出如下的程式碼:先上鎖,發現條件不滿⾜,解鎖,然後等待在條件變數上不就⾏了,如下程式碼:
// 錯誤的設計
pthread_mutex_lock(&mutex);
while (condition_is_false) {
pthread_mutex_unlock(&mutex);
//解鎖之後,等待之前,條件可能已經滿⾜,訊號已經發出,但是該訊號可能被錯過
pthread_cond_wait(&cond);
pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&unlock);
- 由於解鎖和等待不是原⼦操作。調⽤解鎖之後,pthread_ cond_ wait之前,如果已經有其他執行緒獲取到互斥量,摒棄條件滿⾜,傳送了訊號,那麼pthread_ cond_ wait將錯過這個訊號,可能會導致執行緒永遠阻塞在這個pthread_ cond_ wait。所以解鎖和等待必須是⼀個原⼦操作。
- nt pthread_ cond_ wait(pthread_ cond_ t *cond,pthread_ mutex_ t * mutex); 進⼊該函式後,會去看條件量等於0不?等於,就把互斥量變成1,直到cond_ wait返回,把條件量改成1,把互斥量恢復成原樣。
條件變數使⽤規範
- 等待條件程式碼
pthread_mutex_lock(&mutex);
while (條件為假)
pthread_cond_wait(cond, mutex);
修改條件
pthread_mutex_unlock(&mutex);
- 給條件傳送訊號程式碼
pthread_mutex_lock(&mutex);
設定條件為真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);