Michael-Scott 無鎖佇列(the Michael-Scott lock-free queue)
/ MY-kuhl SKOT /
堆疊只碰一端,這就是 Treiber 堆疊只需要一個原子指標的原因。佇列(queue)更難:它是先進先出,你在一端(尾 tail)加入、在另一端(頭 head)移除,現在必須在並行存取下同時維持兩個指標一致。Michael-Scott 佇列由 Maged Michael 與 Michael Scott 於 1996 年發表,是這個問題經典且廣為使用的無鎖解法,許多正式環境的並行佇列都以它為基礎(包括 Java 的 ConcurrentLinkedQueue)。
它的兩個構想值得理解。第一,頭部永遠擺一個哨兵(dummy)節點,使佇列在指標意義上從不真正為空,於是 enqueue 與 dequeue 永遠不必為了空佇列而互相做特例處理。第二,也很巧妙:一次 enqueue 需要兩個無法湊成一個原子步驟的動作——先把新節點連到最後一個節點,再把 tail 擺去指它——而演算法靠讓其他每個執行緒「協助」(helping)來撐過某執行緒在兩步之間暫停:若任何執行緒注意到 tail 落後了(它的 next 不是空),它會先用 CAS 把 tail 推進,再做自己的事。於是一個做到一半的 enqueue 總會被下一個抵達者完成,結構絕不會停在壞掉的狀態。
這種協助正是合作完成(cooperative-completion)技術,也正是它讓佇列成為無鎖、而非僅僅無障礙的原因:沒有任何執行緒的暫停能卡死佇列,因為下一個執行緒會收拾善後。不過與 Treiber 堆疊相同的警告依然成立——dequeue 路徑會讀取另一執行緒可能釋放的節點欄位,所以正確的實作會把 MS 佇列搭配危害指標或 epoch 式回收。它是一個優美、被深入研究的演算法,也是一個長存的提醒:一個正確的無鎖佇列究竟需要多少謹慎。
/* Enqueue:先連結再擺 tail;任何執行緒都會幫忙完成落後的 tail。 */ for (;;) { Node *t = tail, *next = t->next; if (next == NULL) { if (CAS(&t->next, NULL, n)) { CAS(&tail, t, n); return; } /* 完成 */ } else { CAS(&tail, t, next); /* 協助:tail 落後了,把它往前推 */ } }
else 分支就是協助步驟:誰發現 tail 落後就推進它,於是停擺的 enqueue 者永遠卡不死佇列。
哨兵節點與「幫忙推進落後 tail」這一步使它成為無鎖;若沒有安全回收(危害指標或 epoch),dequeue 路徑仍是釋放後使用。