條件變數(condition variable)
有時一條執行緒持有著鎖,卻發現自己得等到某個條件變成真才能繼續——它想讀的佇列是空的、它想填的緩衝區是滿的。在持有鎖的情況下對那個條件忙碌等待,是雙重錯誤:既浪費 CPU,又一直握著鎖,使「本可讓條件成真」的那條執行緒永遠進不來。條件變數正是解決這件事。它是一間總是與某把互斥鎖配對的等候室,讓一條執行緒原子地釋放鎖並睡去,直到另一條執行緒發信號表示條件現在或許成立了。
它有三個操作。wait(cv, lock):呼叫者(必須已經持有 lock)原子地釋放 lock 並在 cv 上阻塞;稍後被喚醒時,它會在返回前重新取得 lock。「原子地釋放並睡去」是關鍵訣竅——它堵住了那個原本可能讓喚醒被錯過的空隙。signal(cv):喚醒一條在 cv 上等待的執行緒(若有)。broadcast(cv):喚醒所有在 cv 上等待的執行緒。標準樣式是:取得鎖,然後 while (條件為假) wait(cv, lock);,接著行動,再釋放。另一邊的執行緒則改動狀態,然後 signal 或 broadcast 讓等待者重新檢查。
兩條不可妥協的規則,都是常見的錯誤來源。第一,永遠在 while 迴圈裡重新檢查條件,絕不要用單一的 if。執行緒可能因為「你的條件成真」以外的原因被喚醒(另一個等待者搶先到、把資源消耗掉了;有些系統還允許「假喚醒」),所以醒來後它必須重新測試條件、必要時再次 wait。第二,條件變數是無狀態、無記憶的:在沒人等待時發出的 signal 就只是丟失了,不會被記住。這正與號誌相反——號誌的計數會記住信號。因此條件本身必須住在「由互斥鎖守護的共用狀態」裡;條件變數只管睡與醒,從不管條件的真假。
一個工人等待工作:acquire(lock); while (佇列為空) wait(cv, lock); job = dequeue(); release(lock)。一個提交者則做:acquire(lock); enqueue(job); signal(cv); release(lock)。用 while(而非 if)重新檢查至關重要:在這條工人重新取得鎖之前,另一個工人可能已把那份工作搶走了。
永遠在 while(條件為假) 裡 wait;沒有等待者時的 signal 會丟失,所以條件要住在共用狀態裡。
絕不要在 if 底下 wait——要用 while 重新檢查,因為有假喚醒與被搶走的喚醒。並且記住條件變數沒有記憶:發給「無人」的 signal 永遠消失。