上一節,我們瞭解了如何對線程之間的共用資源進行保護的方法。但是,有些時候,我們需要線上程之間進行同步操作。一個線程等待另一個線程完成某項工作後,再繼續自己的工作。比如,某個線程需要等待一個訊息,或者某個條件變成true。接下來幾節,我們會看到如何使用C++標準庫來做到線程間同步。
等待另一個線程完成的方法有如下幾種:
一個線程不停地查詢某個被mutex保護的貢獻資料區中的標誌的狀態。另一個線程在完成工作後,設定這個標誌。這種方法十分浪費資源,因為第一個線程需要不停的運行而佔用CPU。並且,當第一個線程鎖定mutex後,第二個線程即使完成了工作,也無法立即設定這個標誌,因為mutex被第一個線程鎖定了。 第二種方法是在每次檢查表之後,執行一定時間的休眠。
bool flag;std::mutex m;void wait_for_flag(){ std::unique_lock<std::mutex> lk(m); while(!flag) { lk.unlock(); // 1 解鎖互斥量 std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 2 休眠100ms lk.lock(); // 3 再鎖互斥量 }} 比如,上面得了例子,解鎖mutex後,先睡眠100ms,在再次鎖定mutex。這樣做,比第一種方法要好一點,但是睡眠時間長短很難確定。短了,就跟沒有休眠一樣,很浪費CPU資源。長了,可能這個線程等待的另一個線程已經完成了操作,這個線程都還在休眠。可能會導致類似掉幀之類的問題。
第三種方法是我們所推薦的,使用C++標準庫提供的機制-條件變數來實現等待操作。條件變數關聯到某種事件或者其它條件,一個或多個線程可以通過這個變數來等待條件的發生。某個線程在發現該條件滿足後,可以通知其它因等待該條件而掛起的線程,喚醒它們讓它們繼續執行。
C++標準庫提供了條件變數的兩種實現:std::condition_variable和std::condition_variable_any。它們都被聲明在<condition_variable>庫標頭檔中。它們都需要和std::mutex配合工作而達到同步操作的目的,而區別在於,前者必須和std::mutex一起工作,而後者和可以和任何滿足特定需求的類似於mutex的模組一起工作。std::condition_variable_any更通用,但它需要更多的記憶體,對作業系統的效能和資源也有更大的影響。大多數情況下,std::condition_variable就夠了。 下面是一個使用std::condition_variable來實現線程間同步的執行個體。data_preparation_thread負責準備資料,接收使用者的輸入,一旦收到資料,通知data_processing_thread來進行處理。當使用者輸入q時,程式退出。
#include <iostream>#include <mutex>#include <condition_variable>#include <thread>#include <queue>static bool more = true;bool more_data_to_prepare(){return more;}struct data_chunk{char m_data = 'q';data_chunk(char c) : m_data(c) {}};data_chunk prepare_data(){std::cout << "data_preparation_thread prepare_data"<< std::endl;char x = 'q';std::cin >> x;if (x == 'q'){more = false;} return data_chunk(x);}void process(data_chunk& data){std::cout << "process data: " << data.m_data << std::endl;}bool is_last_chunk(data_chunk& data){if (data.m_data == 'q') { return true;}return false;}std::mutex mut;std::queue<data_chunk> data_queue;// 用於線程間通訊的隊列 std::condition_variable data_cond;void data_preparation_thread(){ while(more_data_to_prepare()) { std::cout << "data_preparation_thread while" << std::endl; data_chunk const data=prepare_data(); std::lock_guard<std::mutex> lk(mut); // 資料準備好後,使用lock_guard來鎖定訊號量,將資料插入隊列之中 data_queue.push(data); std::cout << "data_preparation_thread notify_one" << std::endl; // 通過條件變數通知其它等待的線程 data_cond.notify_one(); }}void data_processing_thread(){ while(true) { std::cout << "data_processing_thread while" << std::endl; // 使用unique_lock,因為我們需要在取得資料之後,處理資料之間,解鎖mutex std::unique_lock<std::mutex> lk(mut); std::cout << "data_processing_thread before wait" << std::endl; // 等待條件滿足,unique_lock和Lambda函數,判斷資料隊列是否為空白 data_cond.wait(lk,[]{return !data_queue.empty();}); std::cout << "data_processing_thread pass wait" << std::endl; data_chunk data=data_queue.front(); data_queue.pop(); // 處理資料需要較多時間,所以先解鎖mutex lk.unlock(); std::cout << "data_processing_thread process data" << std::endl; process(data); if(is_last_chunk(data)) break; }}int main(){std::cout << "main" << std::endl; std::thread t1(data_preparation_thread); std::thread t2(data_processing_thread); t1.join(); t2.join();}程式執行效果如下:輸入a,data_preparation_thread喚醒data_processing_thread,輸入q,data_processing_thread再次被data_preparation_thread喚醒,處理資料之後,程式結束。
maindata_preparation_thread whiledata_preparation_thread prepare_datadata_processing_thread whiledata_processing_thread before waitadata_preparation_thread notify_onedata_preparation_thread whiledata_preparation_thread prepare_datadata_processing_thread pass waitdata_processing_thread process dataprocess data: adata_processing_thread whiledata_processing_thread before waitqdata_preparation_thread notify_onedata_processing_thread pass waitdata_processing_thread process dataprocess data: q--------------------------------Process exited after 4.063 seconds with return value 0Press any key to continue . . .