C 11執行緒間同步方式

2021-10-02 17:43:11 字數 3795 閱讀 9915

執行緒間為什麼需要同步?直接來看乙個例子:

int a =0;

void

foo()}

intmain()

**很簡單,建立兩個程序執行foo函式,foo函式的功能是對全域性變數a進行自增,我們所預期的答案是20000000。但是實際執行結果卻幾乎不可能得到這個值,執行結果如下:

16721621

183

a的最終結果為16721621,共使用了183毫秒的時間。在兩個執行緒對a進行自增的過程中可能會因為執行緒排程的問題使得最終結果並不正確。比如當前a的值為1,執行緒x現在將a的值讀到暫存器中,而執行緒y也將a讀到暫存器中,完成了自增並將新的值放入記憶體中,現在a的值為2,而執行緒x現在也對暫存器中的值進行自增,並將得到的結果放入記憶體中,a的值為2。可以看到兩個執行緒都對a進行了自增,但是卻得到的錯誤的結果。

這種情況便需要對執行緒間進行同步。

其實在apue的學習中已經講過了執行緒間的同步方式,共有五種,分別是互斥鎖,自旋鎖,讀寫鎖,條件變數和屏障

但是unix系統中的同步方式編寫的**並不能跨平台,都是c語言風格的結構,使用起來並不方便。所以在後來編寫**的過程中更喜歡使用c++11執行緒庫和同步方式,不僅介面簡單,而且也能在誇平台上使用。

2.1 互斥鎖

mutex _mutex;

_mutex.

lock()

;//加鎖

_mutex.

unlock()

;//解鎖

_mutex.

try_lock()

;//嘗試加鎖,成功返回bool,失敗返回false不阻塞

包含mutex標頭檔案後,就可以使用mutex類。相比起unix風格(介面名字複雜,且需要初始化互斥鎖)要方便不少。

現在使用互斥鎖來實現兩個執行緒對同一變數自增的功能:

int a =0;

mutex _mutex;

void

foo()}

intmain()

只有獲得鎖之後才能對資料a進行操作,執行結果如下:

20000000

7888

可以看到這次得到了我們期望的正確答案,但是使用的時間卻大大增加,使用了7888ms,是之前的40倍。造成這種現象的原因:

鎖的爭用造成了執行緒阻塞

互斥鎖的獲取需要陷入核心態,即每次上鎖解鎖,都需要從使用者態進入核心態,再回到使用者態。而foo函式本身執行自增操作只需要兩條指令就能完成,而核心態的切換可能需要上百條指令。

要實現更加高效的同步就需要引入下乙個內容,自旋鎖。

2.2 自旋鎖

自旋鎖是一種忙等形式的鎖,會再使用者態不同的詢問鎖是否可以獲取,不會陷入到核心態中,所以更加高效。缺點是可能會對cpu資源造成浪費。但是在c++11中並沒有直接提供自旋鎖的實現。但是在c++11中提供了原子操作的實現,可以借助原子操作實現簡單的自旋鎖。

atomic_flag flag;

int a =0;

void

foo(

)//加鎖

a +=1

; flag.

clear()

;//解鎖}}

intmain()

atomic_flag是乙個原子變數,共有set和clear兩種狀態。在clear狀態下test_and_set會將其狀態置於set並返回false,在set狀態下test_and_set會返回true。可以看到自旋鎖其實和cas方式實現的樂觀鎖很相似,使用原子操作改變標誌為的值,並不斷地輪詢標誌位。

執行結果如下:

20000000

6023

可以看到得到了預期的正確答案,但是在時間效能上,並沒有比互斥鎖高出特別多。感覺很疑惑,查了一些資料,找到乙個比較合理的解釋:現代作業系統中的互斥鎖是一種更加綜合的鎖,是互斥鎖和自旋鎖的結合,在無法獲得鎖時會先自旋一段時間,如果在這段時間中獲得了鎖便繼續執行,如果沒有獲得便陷入核心阻塞程序。

自旋鎖和互斥鎖的優缺點和使用場景

互斥鎖不會浪費cpu資源,在無法獲得鎖時使執行緒阻塞,將cpu讓給其他執行緒使用。比如多個執行緒使用印表機等公共資源時,應該使用互斥鎖,因為等待時間較長,不能讓cpu長時間的浪費。

自旋鎖效率更高,但是長時間的自旋可能會使cpu得不到充分的應用。在臨界區**較少,執行速度快的時候應該使用自旋鎖。比如多執行緒使用malloc申請記憶體時,內部可能使用的是自旋鎖,因為記憶體分配是乙個很快速的過程。

2.3 條件變數

條件變數在c++11中有現成的類可以使用,比unix風格的介面更加方便。用法和unix的條件變數類似,需要配合互斥鎖使用。

如果不懂條件變數原理及使用的可以看看這篇部落格:c++條件變數實現多執行緒順序列印

現在我們使用c++11的條件變數完成三個執行緒順序列印0,1,2:

condition_variable cond;

mutex _mutex;

int a =0;

void

first()

}void

second()

}void

third()

}int

main()

其中unique_lock是對mutex的一種raii使用手法,來看看unique_lock的建構函式和析構函式:

explicit

unique_lock

(_mutex& _mtx)

:_pmtx

(_std addressof

(_mtx)),

_owns

(false)~

unique_lock()

noexcept

可以看到在unique_lock構造時自動加鎖,析構時完成鎖的釋放,使用這種程式設計技法可以保證在推出臨界區時鎖一定會被釋放。

2.4 屏障

屏障(barrier)是使用者協調多個執行緒並行工作的同步機制。屏障允許每個執行緒等待,直到所有的合作執行緒都達到某一點,然後從該點繼續執行。pthread_join函式就是一種屏障,允許乙個執行緒等待,直到另乙個執行緒退出。

但是屏障物件的概念更廣,它們允許任意數量的執行緒等待,直到所有的執行緒完成處理工作,而執行緒不需要退出。所有執行緒達到屏障後可以接著工作。

不過屏障本身很少被使用,可以使用條件變數和互斥鎖完成屏障的功能:

condition_variable cond;

mutex _mutex;

//unique_locklck(_mutex);

int _latch =3;

void

wait()

void

countdown()

void

thread1()

void

thread2()

void

thread3()

intmain()

執行結果如下:

wait...

thread1 finish

thread2 finish

thread3 finish

all thread finish

完成了屏障的功能,當執行緒123都執行完成後,main執行緒才會繼續執行。

除去上述的同步方式之外,unix系統還提供讀寫鎖用於執行緒同步,在c++11中也沒有對應的介面,不過實現較為複雜,下次再更新。

c 11執行緒退出方式

c 11執行緒強制退出執行緒的介面 也不贊成這樣退出 只能自己實現退出邏輯,以下示例是開發中個人覺得比較簡單好用的一種方式 include include include include include include class test void fn1 std cout thread std ...

c 11執行緒間引數傳遞

建立的工作執行緒不止乙個,執行緒根據編號來確定工作內容。每個執行緒都需要知道自己的編號。執行緒中有很多容易犯錯的寫法 例子1多執行緒需要執行的函式 voidmy print constint i,char p mybuff 主函式的寫法 intmvar 1 int mvary mvar charmy...

C 11多執行緒std thread建立方式

include include include include include include using namespace std pragma region c 11 thread基本建立方法 if 1 案例一 void my print 案例二 class ta ta 案例二 void op...