線程的知識點太多,太重要,所以分成三部分進行總結學習
線程安全
多個線程併發同一段代碼時,不會出現不同的結果。常見對全局變量或者靜態變量進行操作,並且沒有鎖保護的情況下,會出現該問題。
多個線程對臨界資源進行競爭操作時若不會造成數據二義性時則線程安全;否則,此時就是不安全的
如何實現線程安全
常見的線程安全的情況
- 每個線程對全局變量或者靜態變量只有讀取的權限,而沒有寫入的權限,一般來說這些線程是安全的
- 類或者接口對於線程來說都是原子操作
- 多個線程之間的切換不會導致該接口的執行結果存在二義性
常見的線程不安全的情況
- 不保護共享變量的函數
- 函數狀態隨着被調用,狀態發生變化的函數
- 返回指向靜態變量指針的函數
- 調用線程不安全函數的函數
在網上調研過程中看到一個總結:減少對臨界資源的依賴,儘量避免訪問全局變量,靜態變量或其它共享資源,如果必須要使用共享資源,所有使用到的地方必須要進行互斥鎖 (Mutex) 保護
所以當對臨界資源使用時,儘量在必須的地方使用鎖的保護
對臨界資源又有兩種訪問,分別是同步訪問和互斥訪問
同步:臨界資源的合理訪問
異步:臨界資源同一時間的唯一訪問
互斥鎖
互斥鎖的操作就是1/0
的操作
一個0或者1的計數器。1可以表示加鎖,加鎖就是計數-1;操作完畢之後要解鎖,解鎖就是計數+1;
0表示不可以加鎖,不能加鎖則等待
//互斥鎖的接口
int pthread_mutex_destroy(pthread_mutex_t *mutex);
//函數應銷燬mutex引用的mutex對象
//注意!!!
//銷燬已解鎖的已初始化互斥體應是安全的。試圖銷燬鎖定的互斥體會導致未定義的行爲。
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
//mutex:互斥鎖變量
//attr:屬性,通常爲NULL
//應使用attr指定的屬性初始化mutex引用的mutex
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//宏PTHREAD_MUTEX_INITIALIZER來靜態的初始化鎖
//互斥鎖變量不一定非要全局變量--只要保證要互斥的線程都能訪問到就行
int pthread_mutex_lock(pthread_mutex_t *mutex);
//鎖定mutex引用的mutex對象。如果互斥體已被鎖定,則調用線程應阻塞,直到互斥體可用。此操作將返回互斥對象引用的互斥對象處於鎖定狀態,調用線程作爲其所有者。
int pthread_mutex_trylock(pthread_mutex_t *mutex);
//函數應等同於pthread_mutex_lock(),但如果mutex引用的mutex對象當前被鎖定(由任何線程,包括當前線程),則調用應立即返回。
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//函數應釋放mutex引用的mutex對象。互斥體的釋放方式取決於互斥體的type屬性。如果在調用pthread_mutex_unlock()時,mutex引用的mutex對象上有線程被阻塞,導致mutex可用,調度策略應確定哪個線程應獲取mutex。
互斥鎖的操作步驟
- 定義互斥鎖變量
- 初始化互斥鎖變量
- 加鎖
- 解鎖
- 銷燬互斥鎖
通過一個互斥鎖Demo來感受一下鎖的使用
//模擬黃牛搶票,100張票,共有四個黃牛在搶票
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
int ticket = 100;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//定義初始化鎖
void* thr_start(void* arg){
while(1){
pthread_mutex_lock(&mutex);
if(ticket > 0){
usleep(1000);
printf("yellow bull : %d----get ticket : %d\n",(int)arg,ticket);
ticket--;
}else{
pthread_mutex_unlock(&mutex);
pthread_exit(NULL);
}
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main(int argc, char* argv[]){
pthread_t tid[4];
int i = 0,ret;
pthread_mutex_init(&mutex,NULL);
for(;i < 4; ++i){
ret = pthread_create(&tid[i],NULL,thr_start,(void*)i);
if(ret != 0){
printf("yellow bull no exit!");
return -1;
}
}
for(i = 0;i < 4;++i){
pthread_join(tid[i],NULL);
}
pthread_mutex_destroy(&mutex);
return 0;
}
這種情況黃牛搶票是比較容易的,一般只有一個黃牛能全搶到票。
但是如果把鎖去掉
這樣搶票就很混亂,因爲沒有了保護。所以鎖的使用是在共享資源對它進行保護,換句話說加鎖是爲了保護資源,所以在這個代碼中就將搶票的操作進行加鎖保護。這樣就只有一個黃牛可以搶到票。
死鎖
在進行加鎖的過程中很有可能發生死鎖的情況下。
在一組進程中的各個進程均佔有不會釋放的資源,但因互相申請被其他的進程所佔用不會釋放的資源而處於一種永久等待的狀態
死鎖的四個條件(重點)
1、互斥條件:一個資源一次只能被一個執行流使用
我操作的時候別人不能操作
2、請求與保持條件:一個執行流因請求資源而阻塞時,對已獲得的資源保持不變
拿着手裏的,但是請求其他的,其他的請求不到,手裏拿着的也不放開
3、不可剝奪條件:一個執行流已獲得的資源,在未使用完之前,不能強行剝奪
我的鎖,別人不能釋放
4、循環等待條件:若干執行流之間形成一種頭尾相接的循環等待資源的關係
指在發生死鎖時,必然存在一個進程資源的環形鏈,即進程集合{P0,P1,P2,···,Pn}中的P0正在等待一個P1佔用的資源;P1正在等待P2佔用的資源,Pn正在等待已被P0佔用的資源
死鎖的產生與處理
當加鎖或者解鎖順序不同時會發生死鎖的情況;對鎖資源的競爭以及進程/線程的加鎖的推進順序b不當
當以上四種條件被破壞時,可以預防死鎖的產生
避免死鎖的方法可以通過:死鎖檢測算法,銀行家算法(推薦王道視頻學習)
同步的實現
條件變量是線程同步的一種手段,條件變量用來自動阻塞一個線程,直到條件滿足被觸發爲止。通常情況下條件變量和互斥鎖同時使用
條件變量使我們可以睡眠等待某種條件出現。條件變量利用線程間共享的全局變量進行同步的一種機制,主要包括兩個動作:
1、一個/多個線程等待“條件變量的條件成立”而掛起;線程1如果操作條件滿足,則操作,否則進行等待。
2、另一個線程使“條件成立”信號;線程2促使條件滿足,喚醒等待的線程。
如果沒有資源則等待(死等),生產資源後喚醒等待。
//條件變量的接口
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
//條件變量初始化,一般attr默認爲NULL
//使用attr引用的屬性初始化cond引用的條件變量。如果attr爲空,則使用默認條件變量屬性;效果與傳遞默認條件變量屬性對象的地址相同。初始化成功後,條件變量的狀態將被初始化。
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
//靜態初始化條件變量
int pthread_cond_destroy(pthread_cond_t *cond);
//銷燬由cond指定的給定的條件變量
//銷燬當前未阻塞線程的初始化條件變量是安全的。
//試圖銷燬當前阻止其他線程的條件變量會導致未定義的行爲。
int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
//abstime:限時等待時長,限時等待時長,超時後則返回
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
//解鎖後的掛起操作(原子操作),有可能還沒來得及掛起就已經有人喚醒--白喚醒--導致了死鎖
int pthread_cond_signal(pthread_cond_t *cond);
//喚醒至少一個等待的
int pthread_cond_boardcast(pthread_cond_t *cond);
//廣播喚醒,喚醒所有等待的人
條件變量的步驟:
1、定義條件變量
2、初始化條件變量
3、等待\喚醒定義的條件變量
4、銷燬條件變量
//模擬一個skr與cxk使用比賽舞臺的Demo
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
int have_stage = 1;
pthread_cond_t skr;
pthread_cond_t cxk;
pthread_mutex_t mutex;
void* thr_skr(void* arg){//skr此時要上臺dancing
while(1){
pthread_mutex_lock(&mutex);
//若此時舞臺有人用,那麼skr進行等待
while(have_stage == 1){
pthread_cond_wait(&skr,&mutex);
}
//舞臺被人使用了,此時0;因爲之前1,代表可以使用
printf("skr~~ is freestyle!!!\n");
sleep(1);
//跳完舞后舞臺空了出來
have_stage += 1;
//有舞臺了,叫cxk來使用
pthread_cond_signal(&cxk);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void* thr_cxk(void* arg){
while(1){
pthread_mutex_lock(&mutex);
//沒有舞臺,那麼在這裏等待
while(have_stage == 0){
pthread_cond_wait(&cxk,&mutex);
}
//有了舞臺就是可以唱跳rap籃球了。。
printf("cxk~~ is singing,dancing,playing rapping and basketball!!\n");
sleep(1);
have_stage -= 1;
//跳完還想跳。。因此叫skr快跳完換他跳。。
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&skr);//喚醒skr,
}
return NULL;
} int main(int argc,char * argv[]){
pthread_t tid1,tid2;
int ret;
pthread_cond_init(&skr,NULL);
pthread_cond_init(&cxk,NULL);
pthread_mutex_init(&mutex,NULL);
int i = 0;
for(i = 0;i < 2;i++){
ret = pthread_create(&tid1,NULL,thr_skr,NULL);
if(ret != 0){
printf("skr error");
return -1;
}
}
for(i = 0;i < 2; i++){
ret = pthread_create(&tid2,NULL,thr_cxk,NULL);
if(ret != 0){
printf("cxk error");
return -1;
}
}
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
pthread_cond_destroy(&skr);
pthread_cond_destroy(&cxk);
pthread_mutex_destroy(&mutex);
return 0;
}
運行結果可以看到cxk和skr交替互斥的表演節目。。。
那麼互斥量(mutex)保護的是什麼?其實保護的是變量條件(have_stage),當互斥量被成功lock後我們就可以放心的去讀取變量條件,這樣就不用在擔心在這期間變量條件會被其他線程修改。如果變量條件不滿足條件,當前線程阻塞,等待其他線程釋放條件成立信號,並釋放已經lock的mutex。這樣一來其他線程就有了修改變量條件的機會。當其他線程釋放條件成立信號後,pthread_cond_wait函數返回,並再次lock
pthread_cond_wait的工作流程可以總結爲:unlock mutex,start waiting -> lock mutex。
while的作用
在變量條件處爲什麼不用if
做判斷而是用while,這是因爲pthread_cond_wait的返回不一定意味着其他線程釋放了條件成立信號。也可能意外返回。這種被稱爲假喚醒,在Linux中帶阻塞功能的system call都會在進程中收到了一個signal後返回。這就是爲什麼使用while來檢查的原因。因爲不能保證wait函數返回的一定就是條件滿足,如果條件不滿足,那麼我們還需要繼續等待
signal條件變量的考慮
解鎖互斥量mutex和發出喚醒信號是兩個單獨的操作,所以就存在一個順序的問題
(1) 按照 unlock(mutex); condition_signal()順序,當等待線程被喚醒時,因爲mutex已經解鎖,因此被喚醒的線程(skr)很容易就鎖住了mutex然後從conditon_wait()中返回了。
(2) 按照 condition_signal(); unlock(mutex)順序,當等待線程被喚醒時,它試圖鎖住mutex,但是如果此時mutex還未解鎖,則線程又進入睡眠,mutex成功解鎖後,此線程在再次被喚醒並鎖住mutex,從而從condition_wait()中返回。