危害指標(hazard pointer)
想像一座共享圖書館:在你拿起一本書閱讀之前,你要在那本書書名旁的公開借閱登記表上寫下你的名字。任何想丟掉這本書的人都會先查那張表;若你的名字在上面,他們就放過這本書、晚點再來。危害指標(hazard pointer)由 Maged Michael 發明,正是無鎖程式碼中安全記憶體回收的這套協定。每個執行緒公開宣告它當下正在使用的指標,而沒有人會釋放有人宣告過的節點。
具體地說,每個執行緒擁有少量固定數目、單一寫入者多讀者的槽位,稱為它的危害指標。讀者端協定是:在你解參考一個從共享結構載入的節點之前,你把那個節點的位址存進你的某個危害槽,然後重新驗證結構是否仍指向它(因為在你的載入與發布之間它可能已經變了)。一旦發布並重新驗證過,你就能安全使用該節點——任何掃描者都會看見你的保留。當你移除一個節點時,你不釋放它;你把它加到每執行緒的退役(retired)清單。你會週期性地執行掃描:把每個執行緒發布的所有危害指標收進一個集合,然後只釋放那些位址「不在」該集合中的退役節點。任何仍被標為危害的就留在清單上,下次掃描再試。
誠實的取捨:危害指標讓回收安全,且記憶體用量有界、可預測(每執行緒只有少量節點能滯留),並且容忍執行緒停擺。但它在讀者端有成本——每次受保護的存取都需要一個儲存來發布危害、一道記憶體屏障與一次重新檢查——這比 RCU 幾乎免費的讀取更重。當你需要有界記憶體與穩健性時,它就是首選,例如在一個無法承受某些 epoch 方案所容許之無上界延遲的長時間執行伺服器中。
/* 讀者:先發布危害,使用節點前再重新驗證。 */ for (;;) { Node *p = atomic_load(&head); hazard[my_id] = p; /* 宣告:我正在使用 p */ if (atomic_load(&head) == p) break;/* 重新檢查:仍相同?那就安全 */ } use(p->data); hazard[my_id] = NULL; /* 完成:釋放保留 */ /* 回收者:唯有沒有執行緒把某退役節點標為危害時才釋放它。 */
先發布再重新驗證,可防止節點在載入與宣告之間被釋放;掃描拒絕釋放任何仍被標為危害的節點。
危害指標讓滯留的記憶體有上界,並容忍停擺的執行緒,但替每次受保護的讀取加上一個儲存、一道屏障與一次重新檢查——比 RCU 的讀取重,記憶體上比樸素的 epoch 方案輕。