條件變數與執行緒安全佇列 (Condition Variables and Thread-Safe Queue)

Overview Table

概念 重點
等待事件的三種方式 忙碌輪詢 / 定時休眠 sleep_for / 條件變數(首選)
std::condition_variable 搭配 std::unique_lock<std::mutex>;wait / notify_one / notify_all
虛假喚醒 (spurious wakeup) wait() 可能無通知而醒來 → 必須用 predicate 重新檢查條件
condition_variable_any 可搭配任何滿足最低要求的鎖;較通用但開銷較大
threadsafe_queue 合併 front()+pop() 消除介面競爭;提供 try_pop / wait_and_pop

等待事件的三種方式

當一個執行緒要等待另一個執行緒完成工作(例如「夜車到站要有人叫醒你」),有三種做法。前兩種都有明顯缺陷,C++ 標準庫提供的條件變數 (condition variable) 是首選:等待的執行緒真正休眠、事件發生時被另一執行緒主動喚醒。

方式 作法 缺點
忙碌輪詢 迴圈中持鎖反覆檢查共享旗標 浪費 CPU 時間;持鎖期間阻礙其他執行緒取得鎖、設定旗標
定時休眠 解鎖 → sleep_for(100ms) → 再上鎖 休眠時間難拿捏:太短照樣浪費 CPU、太長反應延遲(遊戲丟幀)
條件變數 wait() 阻塞,由另一執行緒 notify 喚醒 首選:不耗 CPU、事件發生立即喚醒

定時休眠版的核心模式(休眠前必須解鎖,讓別的執行緒有機會設定旗標):

std::unique_lock<std::mutex> lk(m);
while (!flag) {
    lk.unlock();                                              // 1 解鎖
    std::this_thread::sleep_formilliseconds(100); // 2 休眠
    lk.lock();                                                // 3 再上鎖
}

std::condition_variable 基本用法

兩種實作都宣告於 <condition_variable>,且都必須與互斥鎖一起工作。典型生產者/消費者模式(代碼 4.1):

std::mutex mut;
std::queue<data_chunk> data_queue;
std::condition_variable data_cond;
// 生產者:
std::lock_guard<std::mutex> lk(mut);
data_queue.push(data);
data_cond.notify_one();            // 通知等待者
// 消費者:
std::unique_lock<std::mutex> lk(mut);
data_cond.wait(lk, []{ return !data_queue.empty(); });  // 帶 predicate 等待
生產者執行緒                         消費者執行緒
    │                                    │
 prepare_data()                   unique_lock(mut) 上鎖
    │                             wait(lk, pred)
 lock_guard(mut) 上鎖                ├─ pred==false → 解鎖並阻塞
 push(data) 入佇列                   │
 notify_one() ────── 喚醒 ─────────> │
 (解鎖)                              ├─ 重新上鎖 → 再查 pred
                                     └─ pred==true → 返回(持鎖)
                                  取出資料 → 解鎖 → process(data)
為何用 std::unique_lock 而非 std::lock_guard

等待中的執行緒必須在等待期間解鎖互斥鎖、喚醒後重新上鎖,std::lock_guard 沒有這種彈性。若休眠期間仍持鎖,生產者永遠無法上鎖推入資料,消費者也永遠等不到條件達成。unique_lock 的彈性也適用於處理資料前提早 unlock()——耗時操作不應長時間持鎖。

虛假喚醒 (spurious wakeup) 與 predicate

等待執行緒可能並非回應 notify 而醒來(重新取得鎖並檢查條件),這稱為虛假喚醒。其次數與頻率不確定,所以:

template<typename Predicate>
void minimal_waitmutex>& lk, Predicate pred {
    while (!pred()) { lk.unlock(); lk.lock(); }
}

notify_one vs notify_all

成員函式 喚醒對象 適用情境
notify_one() 至多一個等待執行緒(不保證是哪個) 工作佇列:一份資料只需一個執行緒處理
notify_all() 所有等待執行緒 全體都要回應:共享資料初始化完成、週期性重新初始化 (periodic reinitialization)
通知不保證送達

使用 notify_one() 時不保證特定執行緒被通知到——即使只有一個等待執行緒,它也可能正在處理資料而錯過通知。而且若條件只成立一次(等待執行緒只等這麼一次,之後不再等),條件變數並非最佳選擇,應改用 future(見 04-Synchronizing-Operations/02-Futures-and-Asynchronous-Tasks)。

condition_variable vs condition_variable_any

std::condition_variable std::condition_variable_any
可搭配的鎖 std::unique_lock<std::mutex> 任何滿足最低要求(可 lock/unlock)的鎖型別
開銷 較小 → 首選 體積、效能、系統資源開銷較大
使用時機 一般情況 對鎖的靈活性有特殊要求時

執行緒安全佇列 threadsafe_queue

如同 3.2.3 節的執行緒安全堆疊,std::queue 的介面存在介面競爭條件:front()pop() 分離,兩個執行緒可能讀到同一元素卻各自 pop。因此設計時將兩者合併為單一操作,並依「等不等」提供兩組 pop:

操作 行為 兩種重載
try_pop() 立即返回,即使佇列為空 bool try_pop(T&)(無資料回傳 false)/ std::shared_ptr<T> try_pop()(無資料回傳空指標)
wait_and_pop() 等待到有值可取才返回 void wait_and_pop(T&) / std::shared_ptr<T> wait_and_pop()
push() 上鎖 → 入佇列 → notify_one()
empty() const 成員函式,仍需上鎖
template<typename T>
class threadsafe_queue {
    mutable std::mutex mut;          // const 成員函式也要上鎖 → mutable
    std::queue<T> data_queue;
    std::condition_variable data_cond;
public:
    void push(T v) {
        std::lock_guard<std::mutex> lk(mut);
        data_queue.push(v);
        data_cond.notify_one();
    }
    void wait_and_pop(T& v) {
        std::unique_lock<std::mutex> lk(mut);
        data_cond.wait(lk, [this]{ return !data_queue.empty(); });
        v = data_queue.front(); data_queue.pop();
    }
};
同步封裝在資料結構內部

互斥鎖與條件變數都是佇列的私有成員,呼叫端 push() / wait_and_pop() 不需任何外部同步,同步問題與競爭條件出現的機率大幅降低。這是第 6 章鎖基礎資料結構設計的起點。


Exam/Test Patterns

情境/關鍵字 答案
「等待事件但不想浪費 CPU」 std::condition_variable::wait + predicate
「wait 為何要用 unique_lock」 等待期間需解鎖、喚醒後重新上鎖;lock_guard 不支援
「沒有 notify 也醒來了」 虛假喚醒 → 用 predicate 重查條件;predicate 不可有副作用
「front() 與 pop() 分開呼叫」 介面競爭條件 → 合併為 try_pop / wait_and_pop
「const 成員函式內要上鎖」 互斥鎖宣告為 mutable
「所有等待執行緒都要被喚醒」 notify_all()(初始化完成、週期性重新初始化)
「一次性事件、只等一次」 條件變數非最佳 → 改用 future
「需要搭配自訂鎖型別的條件變數」 std::condition_variable_any(開銷較大)