號誌(semaphore)
/ SEM-uh-for /
想像一個小停車場,門口立著一塊牌子顯示空位數。一輛車進來時把牌子上的數減一、開進去;若牌子顯示零,駕駛就在閘口等到有空位。一輛車離開時把數加一,放一個等待的駕駛進來。號誌對執行緒來說正是這塊計數牌:一個永不低於零的整數,配上兩個讓它減一和加一的原子操作。它由 Edsger Dijkstra 在一九六〇年代發明。
這兩個操作歷史上叫 P(源自荷蘭文,「試著減少」)與 V(「增加」),更好讀的名字是 wait 與 post(或 down 與 up,或 acquire 與 release)。wait/P 把計數減一;若計數已是零,呼叫端的執行緒就阻塞,直到能減一為止。post/V 把計數加一,並喚醒一個被阻塞的等待者(若有)。一個初始為 N 的計數號誌,一次最多放 N 個執行緒通過——非常適合限制對 N 份相同資源的存取(N 條資料庫連線、N 個緩衝槽)。二元號誌的計數只會是 0 或 1,所以它表現得像一把鎖——但與互斥鎖不同,它沒有「擁有者」的概念:任何執行緒都能 post 它,包括一個從沒 wait 過的執行緒。這種無主特性,使號誌成為執行緒間「發信號」的天然工具,由一個執行緒 post、另一個不同的執行緒 wait。
號誌比互斥鎖與條件變數更通用,你可以用它建出這兩者——但這份通用性也是陷阱。因為號誌不帶擁有者、也沒有附帶的鎖不變式,很容易多 post 了一次、或漏掉一次 wait,而產生難察覺的錯誤。現代建議是:要互斥就用互斥鎖,要「等待某狀態」就用條件變數,而把號誌留給「計數一份資源」或「簡單發信號」——在那裡它的計數才真正對應到某個實在的東西。
要把同時下載數上限設為 3:sem_t s; sem_init(&s, 0, 3);,每個工人做 sem_wait(&s); download(); sem_post(&s);。一次最多三個工人佔著名額;第四個會在 sem_wait 阻塞,直到有人 post。
一個計數為 3 的號誌一次放進三個執行緒;wait 減一、post 加一。
號誌沒有擁有者,所以與互斥鎖不同,任何執行緒都能 post 它,也沒有「解開一把你並未持有的鎖」的內建偵測。把二元號誌當互斥鎖用,會失去擁有者檢查與優先權繼承,所以要互斥還是優先用真正的互斥鎖。