生產者-消費者問題(producer-consumer problem)
想像一家忙碌的麵包店。後場的師傅不斷從烤箱拿出新鮮的麵包,放到架子上;前台的客人則不斷從架子上取走麵包。兩邊各自以自己的速度工作,從不必互相對望——中間這個架子讓他們合作,卻不必協調每一個動作。可是架子的空間有限:若師傅太快,架子放滿了,師傅就得暫停;若客人太快,架子空了,客人就得等待。生產者-消費者問題就是這件事的電腦版本:一個或多個產生工作的執行緒,和一個或多個消耗工作的執行緒,透過一個共享緩衝區彼此傳遞項目。
具體而言,生產者把項目放進共享佇列,消費者把項目取出。經典的正確解法用三個同步元件。一個叫 empty 的計數號誌(counting semaphore)初值為緩衝區大小,計算空位數;生產者在放入前做 wait(empty),若緩衝區已滿就被擋住。第二個叫 full 的計數號誌初值為零,計算已填滿的位置;消費者在取出前做 wait(full),若緩衝區為空就被擋住。生產者放入後對 full 做 signal(喚醒等待中的消費者),消費者取出後對 empty 做 signal(喚醒等待中的生產者)。第三個是互斥鎖(mutex),保護真正的放入與取出,使兩個執行緒不會同時破壞佇列內部的指標。順序很重要:先取計數號誌、再取互斥鎖——反過來就可能死結。
一旦學會看出這個模式,到處都是它:網頁伺服器的接受執行緒把連線交給工作執行緒、記錄系統先把訊息緩衝起來再由寫入者刷到磁碟、兩個 Unix 指令之間的管線、影片播放器在螢幕顯示之前先解碼數張畫面。它是把快階段與慢階段解耦的標準做法,讓任一方都不必知道另一方的時序。更深的教訓是:一個選得好的共享緩衝區加上正確的阻塞,能把困難的時序問題化為整潔又可重用的設計。
生產者迴圈:wait(empty); wait(mutex); buffer.put(item); signal(mutex); signal(full)。消費者迴圈:wait(full); wait(mutex); item = buffer.take(); signal(mutex); signal(empty)。
教科書上的號誌解法。注意兩個計數號誌(empty、full)在互斥鎖之外;對調這個巢狀順序就有死結風險。
用一個共享計數器加上「if (count == 0) wait」來取代號誌,是經典的錯誤:在測試計數與進入睡眠之間,另一個執行緒可能改了它(遺失喚醒的競態)。請使用計數號誌,或在迴圈中搭配條件變數。