理解 C++ 中的條件變量(Condition Variable)
在多線程編程中,我們常常需要一個線程等待某個條件的變化,比如等待數據的生成或某個標志位的設置。如果沒有條件變量(condition_variable
),線程可能會使用忙等待(不斷檢查條件是否滿足),這會導致 CPU 資源的浪費。條件變量提供了一種高效的等待機制,使線程在等待條件時進入休眠狀態,不占用 CPU 資源,當條件滿足時被喚醒繼續執行。
條件變量的基本概念
條件變量允許一個或多個線程同時阻塞。一般情況下,生產者線程利用支持 std::mutex
的 std::lock_guard
或 std::unique_lock
修改共享變量后,并通知條件變量。消費者線程獲取同一個 std::mutex
(由 std::unique_lock
持有),并調用 std::condition_variable
的 wait
、wait_for
或 wait_until
。wait
操作會釋放互斥量,同時掛起該線程。當條件變量收到通知、超時到期或發生虛假喚醒時,線程被喚醒,互斥量也會被原子地重新獲取。如果是虛假喚醒,線程應該檢查條件并繼續等待,以保證業務的正確性。
注意事項
-
使用
unique_lock
而不是lock_guard
:
在等待時,管理mutex
使用的是unique_lock
而不是lock_guard
,因為等待時是不持有鎖的。wait
函數會調用mutex
的unlock
函數,之后再睡眠,直到被喚醒后才持有鎖。lock_guard
沒有lock/unlock
接口,所以需要用unique_lock
。 -
偽喚醒:
在對wait
函數的調用中,條件變量可能會對提供的條件檢查任意多次。這發生在互斥元被鎖定的情況下,并且當測試條件返回 true 時就會立即返回。當等待線程重新獲取互斥元并檢測條件時,如果它并非直接響應另一個線程的通知,這就是所謂的偽喚醒(spurious wake)。偽喚醒的次數和頻率根據定義是不確定的。 -
通知丟失:
如果發送方在接收方進入等待狀態之前發送通知,則通知會丟失。
使用場景
-
生產者-消費者問題:
生產者線程生成數據并通知消費者線程處理數據。 -
任務隊列:
多個線程從一個任務隊列中獲取任務并處理,當隊列為空時,線程進入等待狀態,直到有新的任務被添加。 -
事件等待:
一個或多個線程等待某個事件的發生,當事件發生時,喚醒等待的線程進行處理。
std::condition_variable
和 std::condition_variable_any
的區別
-
互斥鎖類型:
std::condition_variable
只與std::unique_lock<std::mutex>
類型的鎖配合使用。這意味著它只能與std::mutex
類型的互斥鎖一起使用。std::condition_variable_any
可以與任何符合基本鎖(BasicLockable)和鎖互換(Lockable)概念的鎖對象配合使用。它不僅可以與std::mutex
配合,還可以與其他類型的鎖(例如std::shared_mutex
、用戶定義的互斥鎖等)一起使用。
-
靈活性:
std::condition_variable
更加專用,提供了一種高效的實現,因為它只支持std::unique_lock<std::mutex>
。std::condition_variable_any
更加通用,提供了更大的靈活性,可以與任何符合要求的鎖一起使用,因此在某些情況下更為便利。
為什么要有 std::condition_variable_any
?
std::condition_variable_any
的引入是為了提供更大的靈活性,允許開發者使用不同類型的鎖來實現同步需求。在某些情況下,開發者可能需要使用自定義的鎖類型或者 std::shared_mutex
這樣的共享鎖,這時 std::condition_variable
就無法滿足需求,而 std::condition_variable_any
則可以適應這些情況。
代碼示例
以下是一個使用條件變量的代碼示例,演示了如何在 C++ 中使用 std::condition_variable
進行線程同步:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <chrono>std::mutex read_mutex_;
std::condition_variable condition_variable_;
bool readFlag_ = false;void do_print_id(int id) {std::unique_lock<std::mutex> uniqueLock(read_mutex_);/*** todo -* 只有當 __pred 條件為 false 時調用 wait() 才會阻塞當前線程,并且在收到其他線程的通知后只有當 __pred 為 true 時才會被解除阻塞* 1. 線程第一次執行到這里時,會執行表達式方法。判斷到 ready 為false。則當前線程阻塞在此,同時解鎖互斥量,不影響其他線程獲取鎖* 2. 當線程被喚醒,首先就是不斷的嘗試重新獲取并加鎖互斥量,若獲取不到鎖就卡在這里反復嘗試加鎖* 3. 若獲取到了鎖,就執行表達式方法,然后繼續往下執行** 函數原型:void condition_variable::wait(unique_lock<mutex>& __lk, _Predicate __pred)*/condition_variable_.wait(uniqueLock, [&] {std::cout << "condition_variable wait.id: " << id << std::endl;return readFlag_;});std::cout << "do_print_id -> thread : " << id << std::endl;
}class QConditionVariable {
public:void task1() {std::thread threads[10];for (int i = 0; i < 10; ++i) {threads[i] = std::thread(do_print_id, i);}for (std::thread &item : threads) {item.detach();}std::this_thread::sleep_for(std::chrono::seconds(2));notify();std::this_thread::sleep_for(std::chrono::seconds(5));std::cout << "task1--end" << std::endl;}void task2() {std::unique_lock<std::mutex> uniqueLock(read_mutex_);std::cv_status cvStatus = condition_variable_.wait_for(uniqueLock, std::chrono::seconds(5));if (cvStatus == std::cv_status::no_timeout) {//表示條件變量等待成功(條件滿足或被通知)。} else if (cvStatus == std::cv_status::timeout) {//表示條件變量等待超時。}}void task3() {std::unique_lock<std::mutex> uniqueLock(read_mutex_);/*** wait_for: 等待特定的時間段,直到被通知或時間到期。* 執行到這里時,當前線程將進入等待狀態,等待最多1秒鐘:* 如果在這1秒鐘內,條件變量 condition_variable_ 被其他線程通知(通常通過 notify_one 或 notify_all),wait_for 會立即返回 std::cv_status::no_timeout,并且 while 循環會終止。* 如果這1秒鐘內沒有收到通知,wait_for 返回 std::cv_status::timeout,循環條件為真,線程繼續執行循環體內的代碼(這里是空的,沒有其他操作),然后再次進入等待。*/while (condition_variable_.wait_for(uniqueLock, std::chrono::seconds(1)) == std::cv_status::timeout) {}}private:void notify() {std::cout << "start----notify" << std::endl;std::unique_lock<std::mutex> uniqueLockNotify(read_mutex_);readFlag_ = true;condition_variable_.notify_all();std::cout << "end----notify" << std::endl;}
};
在這個示例中,我們創建了一個簡單的類 QConditionVariable
,包含了兩個任務 task1
和 task2
。task1
生成 10 個線程,每個線程都會調用 do_print_id
函數,等待條件變量 condition_variable_
的通知。task2
則是一個等待操作示例,等待特定時間段或直到被通知。
通過條件變量,我們可以有效地管理多線程程序中的同步和通信,避免忙等待,提高程序的執行效率。