無鎖與等待無關程式設計

記憶體回收問題(the memory-reclamation problem)

當你從一個無鎖資料結構移除一個節點時,下一個顯而易見的動作就是釋放它。但這裡有個讓無鎖程式設計真正困難的陷阱:就在你解開並釋放某節點的那一刻,可能有「另一個」執行緒在一微秒前讀到了指向同一節點的指標,正要解參考它。若你此刻釋放它,那個執行緒就會讀到已釋放的記憶體——釋放後使用(use-after-free),這是未定義行為,也是一個安全漏洞。你不能就這樣呼叫 free()。這就是記憶體回收問題,它沒有輕鬆的答案。

它之所以困難的深層原因是:在無鎖設計裡,沒有任何「單一時刻」能讓你確信沒人在看某個節點。有鎖時,持鎖的執行緒擁有獨佔存取,所以它移除節點時「知道」沒有別人正讀到一半。無鎖沒有這種互斥——許多執行緒同時在結構中遊走、讀取指標,而你剛從結構可見狀態中解開的節點,可能仍可透過另一執行緒在你解開之前載入的過時指標而被觸及。所以「從結構中移除」與「安全刪除那塊記憶體」是兩個不同的事件,兩者之間的縫隙正是危險所在。

因此每個解法都是一種把釋放延後、直到你能證明沒有執行緒仍持有參考的方式。主要家族各以不同方式做這個證明:危害指標(hazard pointer),每個執行緒發布它當下正在使用的確切指標,意圖回收者掃描並跳過它們;epoch 式回收與靜止狀態(quiescent-state)回收,你等到每個執行緒都通過了一個它不持有任何參考的安全點;參考計數(reference counting),直接追蹤持有者的數目;以及 RCU,等待一個寬限期(grace period),其後所有既存的讀者都已完成。它們在讀者成本、回收延遲、與滯留待釋放的記憶體之間做取捨——但全都只為回答一個問題:到底什麼時候釋放這個節點才終於安全?

執行緒 R:p = head; /* 載入了指向節點 X 的指標 */ 執行緒 W:解開 X;free(X); /* X 現在沒了 */ 執行緒 R:使用 *p; /* 釋放後使用:p 指向已釋放的記憶體 */ /* 修法:別立即釋放。把 X 退役(retire),唯有當沒有執行緒 還可能持有指向它的指標時才釋放(危害指標/epoch/RCU)。 */

從結構移除節點與釋放它的記憶體是兩個分開的事件;回收問題就是安全地跨越這個縫隙。

有垃圾回收的語言把這個問題藏了起來,因為 GC 不會回收任何人還能觸及的節點。在 C、C++ 與不安全的 Rust 中,你必須自己實作回收;沒有白吃的午餐。

又稱
safe memory reclamationthe deferred-free problemSMR安全記憶體回收