1. 程式人生 > >併發易混淆概念總結

併發易混淆概念總結

競爭

執行緒A執行邏輯經過x邏輯,執行緒B執行邏輯經過y邏輯。

競爭: 程式執行結果的正確性,取決於B的y邏輯必須要在A的x邏輯前執行,此時就發生了競爭。

感覺這麼解釋還是比較抽象,下面通過一個c語言的例子來解釋:

A執行緒通過for迴圈建立多個對等執行緒,x邏輯表示建立對等執行緒並傳遞引數。

B執行緒得到A執行緒傳遞的引數,y邏輯表示列印傳遞的引數。

#include "test.h"
#define N 4
void *thread(void vargp);
//以下為A執行緒
int main() {
  pthread_t tid[N];
  int i;

  for(i = 0
; i < N; i++) {//執行緒A的x邏輯 Pthread_create(&tid[i], NULL, thread, &i); } for(i = 0; i < N; i++) { Pthread_join(tid, NULL); //等待建立的對等執行緒結束 } exit(0); } //執行緒B void *thread(void *vargp) { int myid = *((int*)vargp);//執行緒B的y邏輯 printf("Hello from thread %\n", myid); return NULL
; }

實際結果:

>./test
Hello from thread 1
Hello from thread 3
Hello from thread 2
Hello from thread 3

原因:

主執行緒(A)和對等執行緒(B)之間存在競爭,導致程式最終執行的結果和預期的是不一致的。A執行緒傳遞一個指向本地棧變數的i指標到B 執行緒。如果B執行緒y邏輯(vargp引數的間接引用和賦值)能夠每次都在A執行緒x邏輯(對i加1的操作)那麼myid就能夠得到正確的ID。

解決方案:

可以將執行緒A的&i的傳遞改為傳遞堆空間的值,注意在B執行緒必須釋放堆的記憶體,否則會造成記憶體洩露。

執行緒安全函式

執行緒安全函式:一個函式被稱為執行緒安全的, 當且僅當被多個併發執行緒反覆地呼叫時,它會一直產生正確的結果。

執行緒安全不是很好判斷,我們先來理解下它的補集——執行緒不安全 。一個函式不是執行緒安全就是執行緒不安全的。所以只要學會判斷一個函式是不是執行緒不安全的就可以了。

定義四類執行緒不安全的函式類

  • 不保護共享變數的函式

    這麼說還是很抽象,舉個例子。執行緒A,執行緒B,全域性變數x,執行緒不安全函式func()對變數x執行x++操作,A、B兩個執行緒均要呼叫func(),如果func()沒有使用鎖或者訊號量來保護這個全域性的變數x,那麼該函式就是執行緒不安全的。

  • 保持跨越多個呼叫狀態的函式

    變數next_seed, 函式rand(),函式srand(),第一步:srand()會初始化next_seed變數,第二步:rand()函式會依賴上傳初始化後的next_seed變數進行處理然後返回。這就是跨越多個呼叫狀態的函式 的理解。

    unsigned next_seed = 1;
    
    unsigned rand(void) {
    next_seed = next_seed *1103515245 + 12543;
    return (unsigned) (next_seed>>16)%32768;
    }
    
    void srand(unsigned new_seed) {
    next_seed = new_seed
    }
  • 返回指向靜態變數的指標的函式。

    某類函式(ctime、gethostbytname),將計算結果放在一個static變數中,然後返回一個指向該變數的指標。如果在單執行緒中,使用該類函式沒有任何問題,但是如果是在多執行緒中A執行緒返回的static變數的值,可能已經被B執行緒修改過。

  • 呼叫執行緒不安全函式的函式

    問題: 如果函式f呼叫執行緒不安全函式g,那麼f一定是執行緒不安全的嗎?

    答案: 視情況而定,如果是第一種和第三種情況,用一個互斥鎖保護呼叫位置和任何得到的共享資料,f仍有可能是執行緒安全的。如果是第二種情況(依賴於跨越多次呼叫的狀態),那麼f也不是執行緒安全的。

可重入函式

可重入函式,其特點在於它們具有被多個執行緒呼叫時,不會引起任何共享資料的特性。

感覺說完了,你不能說它的定義是錯的但是還是不清楚什麼函式是可重入函式。簡單點,概念不要這麼複雜簡單點,就是這個函式能夠被再次進入唄!

舉個例子:

執行緒A– ———–>函式F1()————-中斷—————–>函式F1()

*可重入函式也可以這樣理解,重入即表示重複進入,首先它意味著這個函式可以被中斷,其次意味著它除了使用自己棧上的變數以外不依賴於任何環境(包括static),這樣的函式就是purecode(純程式碼)可重入,可以允許有多個該函式的副本在執行,由於它們使用的是分離的棧,所以不會互相干擾。如果確實需要訪問全域性變數(包括static),一定要注意實施互斥手段。可重入函式在並行執行環境中非常重要,但是一般要為訪問全域性變數付出一些效能代價

可重入函式

總結下執行緒安全、執行緒不安全和可重入函式的關係。

執行緒

可重入函式一定是執行緒安全函式,但執行緒安全函式不一定是可重入函式。其實很好理解,可重入函式一定是沒有共享全域性變數的符合執行緒安全函式的第一和第三點要求。

死鎖

什麼是死鎖

死鎖:是指兩個或兩個以上的程序(或執行緒)在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的程序稱為死鎖程序。

死鎖發生的條件

  • 互斥條件:執行緒對資源的訪問是排他性的,如果一個執行緒對佔用了某資源,那麼其他執行緒必須處於等待狀態,直到資源被釋放。

  • 請求和保持條件:執行緒T1**至少已經保持了一個資源R1佔用,但又提出對另一個資源R2請求,而此時,資源R2被其他執行緒T2佔用,於是該執行緒T1也必須等待,但又對自己保持的資源R1不釋放**。

  • 不剝奪條件:執行緒已獲得的資源,在未使用完之前,不能被其他執行緒剝奪,只能在使用完以後由自己釋放

  • 環路等待條件:在死鎖發生時,必然存在一個“程序-資源環形鏈”,即:{p0,p1,p2,…pn},程序p0(或執行緒)等待p1佔用的資源,p1等待p2佔用的資源,pn等待p0佔用的資源。(最直觀的理解是,p0等待p1佔用的資源,而p1而在等待p0佔用的資源,於是兩個程序就相互等待)

舉個例子

執行緒A,執行緒B,互斥鎖S1,互斥鎖S2,A,B執行緒均需要獲取兩個鎖才能繼續執行。

執行緒A獲取互斥鎖S1的使用權,同時執行緒B獲得互斥鎖S2的使用權。此時執行緒A、執行緒B均等待對方釋放自己需要的鎖,但是AB又都不肯釋放自己的鎖,此時就處於死鎖的狀態。

飢餓

什麼是飢餓

飢餓:是指如果執行緒T1佔用了資源R,執行緒T2又請求封鎖R,於是T2等待。T3也請求資源R,當T1釋放了R上的封鎖後,系統首先批准了T3的請求,T2仍然等待。然後T4又請求封鎖R,當T3釋放了R上的封鎖之後,系統又批准了T4的請求……,T2可能永遠等待。

舉個例子

讀者寫者的問題上,如果是讀者優先,那麼寫者就可能一直處於飢餓的狀態。

讀者A先進入,寫者B等待資源,但此時不斷有讀者進入,導致資源一直被佔用無法釋放,於是寫者B就處於飢餓的狀態。

死鎖和飢餓的區別 執行緒是否是一直處於阻塞的狀態還是有可能從阻塞狀態變為非阻塞狀態只是時間未可知。