經典同步問題與並行程式設計

理髮師問題(sleeping barber problem)

/ Dijkstra -> DYKE-struh /

一家小理髮店有一位理髮師、一張理髮椅,和一間擺著固定數量椅子的等候室。沒有客人時,理髮師就在椅子上睡覺。走進來的客人若見理髮師在睡,就把他叫醒;若理髮師正忙,客人便坐進等候室的椅子——除非椅子全坐滿了,那客人就乾脆離開。理髮師剪完一位後查看等候室:有人就叫下一位,沒人就回去睡。這家小店是 Dijkstra 的另一道謎題,模擬一台一次處理一個請求、附有限佇列的伺服器,並照亮了「工作者要去睡」與「客人到來」之間微妙的競態。

危險在於遺失喚醒(lost wakeup)。假設一位客人查看後見理髮師正在剪髮,於是決定去坐等候室——但就在查看與坐下之間的一瞬,理髮師剛好剪完,看了看(似乎仍空的)等候室,便打起盹來。此刻客人坐著等一位睡著的理髮師,而理髮師睡著等一位客人:兩者永遠等下去。標準解法用三個號誌:一個計數號誌 customers(有多少人在等,理髮師對它等待)、一個二元號誌 barbers(理髮師是否空閒),以及一個互斥鎖來保護等待客人數的共享計數,使「查看並坐下」與「剪完並查看」永不糟糕地交錯。

它之所以能與哲學家用餐並列,是因為它隔離出另一類臭蟲——不是握住資源成環導致的死結,而是糾纏任何阻塞佇列的遺失喚醒競態。它本質上是一個單伺服器、有限容量的生產者-消費者問題(客人生產工作,理髮師消費),而理髮店的故事生動地說明了:你必須以不可分割的方式「測試條件並去睡」,絕不能先測試、後睡覺。正是這個教訓,使條件變數得以存在,也是為什麼它的 wait 操作會以不可分割的方式釋放鎖並阻塞。

客人:lock(mutex); if (waiting < chairs) { waiting++; signal(customers); unlock(mutex); wait(barbers); getHaircut(); } else { unlock(mutex); leave(); }。理髮師:wait(customers); lock(mutex); waiting--; signal(barbers); unlock(mutex); cutHair()。

三個號誌加一個互斥鎖。互斥鎖讓「waiting」的檢查與更新成為不可分割的動作,這正是防止遺失喚醒競態的關鍵。

整個重點在於「測試條件、然後阻塞」的不可分割性。若這是兩個分開的步驟,發生在縫隙裡的喚醒就永遠遺失了——這就是為什麼要在 while 迴圈裡(而非 if)對條件變數做 wait。

又称
睡覺的理髮師sleeping barber