無鎖與等待無關程式設計

SPSC 環形緩衝區(與 MPMC)(the SPSC ring buffer and MPMC)

/ ES-PEE-ES-SEE /

你常常不需要一個任何人都能從任何地方推入的完全通用佇列——你恰好只有一個執行緒在生產項目、恰好一個執行緒在消費,就像音訊執行緒餵樣本給音效卡那樣。對這個特例,有一個優美而快速的結構:單生產者單消費者(single-producer, single-consumer, SPSC)環形緩衝區。它是一個被循環使用的固定大小陣列,而且完全不用 CAS 就能做成無鎖。

訣竅在這裡。你保留一個 head 索引(消費者讀取處)與一個 tail 索引(生產者寫入處),兩者都對陣列大小取模而環繞——通常用 2 的次方,使環繞變成便宜的位元遮罩 index & (N - 1),而非除法。生產者永遠只寫 tail;消費者永遠只寫 head。因為每個索引剛好只有一個寫入者,你永遠不需要比較並交換——一個帶釋放(release)順序的單純原子儲存,配上一個帶取得(acquire)順序的單純原子載入,就足以安全地發布每個項目。當推進 tail 會撞上 head 時緩衝區就是滿的,當 head 等於 tail 時就是空的。每個索引只有單一寫入者這個性質,正是 SPSC 如此便宜又如此容易做對的全部原因。

對照的是多生產者多消費者(multi-producer, multi-consumer, MPMC)環形緩衝區,那裡許多執行緒共用每個索引。現在你又回到需要 CAS 來認領一個槽位,而且必須處理兩個執行緒爭奪同一索引、部分寫入的槽位,以及環繞危害——它困難得多,高品質的 MPMC 佇列(如 Dmitry Vyukov 的有界 MPMC 佇列)會用每槽的序號(sequence number)來協調。這個家族也包含 SPMC 與 MPSC。誠實的教訓是:用符合你需求的最弱變體;若你的問題真的是 SPSC,就別為 MPMC 付代價。

/* SPSC:每個索引單一寫入者,意味著不用 CAS,只要釋放儲存/取得載入。 */ bool push(T v) { /* 僅生產者 */ size_t t = tail; /* relaxed:tail 的唯一寫入者就是我們 */ if (((t + 1) & (N - 1)) == atomic_load_acquire(&head)) return false; /* 滿 */ buf[t] = v; atomic_store_release(&tail, (t + 1) & (N - 1)); /* 發布 */ return true; }

用 2 的次方大小,環繞就是位元遮罩;每個索引單一寫入者時,一個釋放儲存即足以發布——不需要 CAS。

SPSC 不需要 CAS,正是因為每個索引只有單一寫入者;一旦你有多個生產者或消費者,就需要 CAS(或每槽序號),難度陡增。

又稱
single-producer single-consumer queuecircular bufferbounded ring queue環形緩衝區單生產者單消費者佇列