Hazard Version
1 lock free算法的內存回收問題
hazard pointer算是比較通用也比較實用的內存回收機制,但是也有缺點,這裏先介紹hazard pointer的做法,然後介紹一種改進方案。
2 hazard pointer
按hazardpointer的做法,一個節點要區分以下兩種狀態
- retired:這個節點被刪掉。在retire之後開始的操作不可能訪問到這個節點,但正在進行中的操作可能還會訪問這個節點。
- reclaimed:這個節點的內存被回收,以後不能再訪問
一個節點在retire之後並不能立即reclaim,因爲正在進行中的操作可能還會訪問這個節點。內存回收的本質就是判斷retired的節點什麼時候可以reclaim。
hazard pointer這種方案的做法就是通過維護一個hazardpointer的集合來判斷一個節點是否還可能被訪問:
- 如果一個節點已經retire,並且這個節點的指針沒有出現在hazard pointer的集合中,那麼就表示不會有任何線程會在未來訪問它。
- 爲了保證性質1,任何一個線程在訪問一個節點之前,需要先把這個節點的指針加入到hazard pointer的集合。
- 雖然初看起來是對的,但實際上2並不能保證性質1,因爲一個線程隨時可能決定要訪問一個節點A,並把節點A的指針加入向hazard pointer集合。 如果在A的指針加入集合之前掃描hazard pointer集合,就會判斷說節點A可以回收。
考慮3中提到的問題:只要在判斷一個節點的指針是否出現在hazard pointer的集合中之前把這個節點的指針加入到hazard pointer集合就足夠了。也就是說一個節點的事件按下面的順序發生肯定是合法的:
- 被加入到hazard pointer集合
- 被retire
- 被reclaim
1和2的時序是需要保證,方法就是:在把一個節點加入到hazard pointer集合後如果發現這個節點已經retire了,就認爲這個節點的指針加入到hazard pointer集合失敗。
3 hazard version
hazard pointer的要求有兩個:
- 需要爲每個要訪問的節點準備一個hazard pointer,對有些數據結構,比如tree來說,hazard pointer的數目比較多
- 需要有一個方法檢測一個節點是否已經retire,對有些數據結構來說,這個判斷不好做。
一個變通的方法是爲每個retire的節點賦予一個retire version,每次操作之前要取一個全局version加入到hazard version集合。只要一個節點的retire version大於任意一個hazard version,那麼這個節點就有可能在未來被訪問。
3.1 具體方案
設置以下全局變量:
- globalVersion: 類型是int64_t, 表示全局version
- : HazardVersionSet:是一個hazard version的集合, 是個multi set
線程A: 執行正常操作
// 操作之前: 獲取全局version,並加入到hazard version set
hazardVersion = atomic_read(&globalVersion)
hazardVersionSet.add(hazardVersion)
mem_barrier();
// ...... 做各種操作, 假設操作過程中retired node被放到retiredNodeList.
// 操作完成: 增加globalVersion, 並把globalVersion賦給每個retired node
retireVersion = atomic_add_and_fetch(&globalVersion)
for p in retiredNodeList:
p.retiredVersion= retireVersion
// 操作完成,把hazardVersion移除
hazardVersionSet.remove(hazardVersion)
// 最後把retired node加入到waitToReclaimNodeList
waitToReclaimNodeList.append(retiredNodeList)
線程B: 執行reclaim
// 假定待回收的節點放在waitToReclaimNodeList
startReclaimVersion = atomic_read(&globalVersion)
minHazardVersion = hazardVersionSet.min()
reclaimableVersion = min(startReclaimVersion,minHazardVersion)
for p in waitToReclaimNodeList:
ifp.retiredVersion < reclaimableVersion
reclaim(p)
3.3 注意的問題
- 爲了保證正確性,線程A必須要在 mem_barrier() 之後重新讀取數據結構的"根指針", 否則就保證不了不會讀取到在 mem_barrier() 之前就retire的節點。
- 上面描述的是樸素的做法,實際使用時要做各種優化