偽喚醒(spurious wakeup)
你設了個鬧鐘,要在水開時響。偶爾你會被驚醒,水卻還是冷的——一次假警報。偽喚醒對等在條件變數上的執行緒來說也是同一回事:它從 wait 回來了,可是根本沒有別的執行緒對它 signal,或者它在等的那個條件其實還不成立。它無緣無故就醒了。
這究竟為什麼會發生?在許多系統上,精準喚醒「對的那個」等待者代價高昂,而某些底層事件(一個信號被遞送給該執行緒,或某種為了省下更昂貴記帳而一次喚醒數個等待者的實作)會讓 wait 提早回傳。C、C++ 與 POSIX 標準明文允許它,這意味著你絕不能假設「從 wait 回來」就代表條件為真。防禦既簡單又是強制的:永遠在一個會重新測試述語的迴圈裡 wait——寫 while (!ready) cond_wait(&cv, &m);,而不是 if (!ready) cond_wait(&cv, &m);。若這次喚醒是偽的,迴圈會看到述語仍為假、直接回去睡;毫髮無傷。
偽喚醒是「等待時搭配述語迴圈」這個樣式不可省略的教科書理由。就算在某個從不產生偽喚醒的平台上,這個迴圈依然是必要的,因為真正的 signal 也會競爭:另一個執行緒可能合法地喚醒了你,但第三個執行緒在你重新取得鎖之前搶到鎖、消耗掉了資源,於是述語又變回假。把喚醒只當成「該重新檢查」的提示——絕不當成保證——能讓你的程式碼在偽喚醒與一般競爭下都正確,且不費額外力氣。
錯:if (queue_empty) cond_wait(&cv, &m); item = dequeue(); ——偽喚醒可能害你從空佇列取出。對:while (queue_empty) cond_wait(&cv, &m); item = dequeue();。
while 迴圈把偽的或過時的喚醒,變成一次無害的重新檢查。
偽喚醒真實存在但罕見;別去「測試」它、也別替它寫特例。唯一正確的應對就是永遠對述語做迴圈,這同時也保護你免於「合法但已過時」的喚醒——所以你根本不需要知道剛剛是哪一種。