一、為何需要訊號量
訊號量用來幹嘛的呢?搜尋答案的話,很多人都會告訴你主要用於執行緒同步的,意思就是執行緒通訊的。簡單來說,比如我運行了2個執行緒A和B,但是我希望B執行緒在A執行緒之前執行,那麼我們就可以用訊號量來處理。有些人可能會疑惑,那麼麻煩幹嘛?你不是要B執行緒先執行嗎?那麼我讓A執行緒休眠一點時間不就可以了嗎?沒錯,這個思路是可以的,但是如果B執行緒也因為某些原因(比如硬體,作業系統的原因)導致延緩執行了,這該怎麼辦?到底A執行緒該休眠多少時間合適呢?所以正確的做法就是在B執行緒阻塞,A執行緒去喚醒這個阻塞執行緒。
看到這兒,看過我前面文章的朋友可能一眼就看出來了這個不就是前面講的生產消費者模型提到的用法嗎?
沒錯,訊號量的實現也是靠條件變數和互斥鎖。
所以雖然C++中並沒有在語言級別上支援訊號量,但同樣的我們可以利用以上兩個來自己實現一個。
這裡我也不得不提一句,條件變數和互斥鎖組合使用真的非常強大,生產消費者模型中用到了,執行緒池中用到了,現在說的訊號量也用到了,所以大家一定要好好掌握條件變數和互斥鎖的使用,它們倆是你在多執行緒世界中縱橫捭闔的利劍。
二、訊號量的實現那麼我們如何用C++來實現一個訊號量呢?
#ifndef _SEMAPHORE_H#define _SEMAPHORE_H#include <mutex>#include <condition_variable>using namespace std; class Semaphore{public: Semaphore(long count = 0) : count(count) {} //V操作,喚醒 void signal() { unique_lock<mutex> unique(mt); ++count; if (count <= 0) cond.notify_one(); } //P操作,阻塞 void wait() { unique_lock<mutex> unique(mt); --count; if (count < 0) cond.wait(unique); } private: mutex mt; condition_variable cond; long count;};#endif
訊號量裡面用到了一個叫PV操作的東西,P操作時阻塞,一般用wait()函式,V操作是喚醒,一般用singal()函式,至於不叫WS操作,反而為什麼叫PV操作呢?網上說是因為提出這一系統方法的人狄克斯特拉用荷蘭文定義的,因為在荷蘭文中,透過叫passeren,釋放叫vrijgeven,PV操作因此得名。對我們來說,這些也沒有太大的意義,記住這些定義就好了,畢竟定義這種東西,是不以我們的意志為轉移的。
寫好了訊號量的介面,那我們如何使用這個訊號量呢?這個就需要我們在外部寫一個多執行緒的呼叫函式來呼叫。
#include "semaphore.h"#include <thread>#include <iostream>using namespace std; Semaphore sem(0); void funA(){ sem.wait(); //do something cout << "funA" << endl;} void funB(){ this_thread::sleep_for(chrono::seconds(1)); //do something cout << "funB" << endl; sem.signal();} int main(){ thread t1(funA); thread t2(funB); t1.join(); t2.join();}
三、訊號量解析這裡我們想讓funB執行緒執行,然後再執行funA,多執行緒是透過時間片輪詢來執行的。
假設先開始跑funA,執行到sem.wait()的時候,進入wait函式可知,count減1,小於0,會發生阻塞,等待其他執行緒喚醒。
然後就會切換到funB,這裡即使休眠了1秒也不會切換到funA,因為那邊阻塞了,沒有其他執行緒喚醒的話就會一直阻塞。funB休眠完之後,就會打印出結果,然後執行sem.signal(),進入signal函式可知,count加1,小於等於0,會喚醒其他阻塞的執行緒。
然後再切到funA,執行後面的操作,打印出結果。
所以這個就一定能保證funB先執行,funA後執行。當然前提是初始化訊號量物件的時候,要初始化為0。
Semaphore sem(0);
訊號量用在多執行緒多工同步的,一個執行緒完成了某一個動作就透過訊號量告訴別的執行緒,別的執行緒再進行某些動作。像這裡funB完成任務之後就透過訊號量的PV操作告訴funA執行緒可以開始任務了。
相關閱讀: