兩年多之前隨手寫了點與 lock free 相關的筆記:1,2,3,4,質量都不是很高其實(讀者見諒),但兩年來陸陸續續竟也有些閱讀量了(可見劍走偏鋒的技巧是多容易吸引眼球)。筆記當中在解決內存釋放和 ABA 問題時提到了 Hazard Pointer 這個東西,有兩三個讀者來信問這是什麼,讓詳細講一下,我想了想,反正之前在看這東西的時候也記了些東西,乾脆整理一下發出來。html
前面寫的那幾篇筆記都來源於 Maged Michael 的學術論文,Hazard pointer 也是他的創想,academic paper 的特色之一就是常常有些美好的假設,關於 hazard pointer 也一樣如此,如下的討論均假設內存模型是 sequential consistent 的,不然仍是問題多多。node
Hazard Pointer(如下簡稱爲 HP) 要解決的核心問題是怎樣安全地釋放內存,該問題的解決在實現無鎖算法時有兩個關鍵的影響:c++
這兩個問題在寫無鎖代碼時基本是沒法避免的,走這條路終會趕上,多少人所以費盡心力窮盡技巧各類花樣,只爲把這問題完全解決。HP 就是這衆多花樣各類技巧中的一種,它的作法以個人愚見也不是很完美,但實現上比較簡單,不依賴具體系統,也不對硬件有特殊要求(固然 CAS 操做仍是要的),從效果上看也湊和,所以不管怎樣是值得參考學習的。算法
在無鎖算法中釋放內存之因此難,主要緣由在於,當一個線程準備釋放一塊內存時,它沒法知道是否另有別的線程也同時持有該塊內存的指針並須要訪問,所以解決這個難點的一個直接想法就是,在每一個線程獲取了一個關鍵內存的指針後,該線程將設置一個標誌,代表"我正在操做這個關鍵數據,大家誰都別給我隨便就釋放了"。固然,這個標誌須要放在一個公共區域,使得任何線程均可以去讀。當另外一個線程想要釋放一塊內存時,它就去把每一個線程的標誌都看一下,看看是否有別的線程也在操做這塊內存,從而決定是否立刻釋放該內存:若是有別的線程在操做該內存,則暫時不釋放,等下次。具體實現以下:編程
HP 算法主要用在實現無鎖的隊列上,所以前面的具體步驟其實基於如下幾個假設:數組
如下爲我從論文裏翻譯過來的僞代碼,入隊列的函數不涉及刪除節點所以不會操做 HP,難點都在處理出隊列的函數上:安全
using hp_t = void*; hp_t hp[N] = {0}; // 如下爲隊列的頭指針。 node_t* top; data_t* Pop() { node_t* t = null; while (true) { t = top; if (t == null) break; // 設置當前線程的 HP hp[this_thread] = t; // 如下這步是必須的,確認了當前 HP 在 t 被釋放前已經被設置到當前線程的 HP 中。 if (t != top) continue; node_t* next = t->next; if (CAS(&top, t, next)) break; } // 已經再也不持有任何節點,需將本身的 HP 設爲空. hp[this_thread] = null; if (t == null) return null data_t* data = t->data; // 嘗試釋放節點 DeleteNode(t); return data; }
以上是出隊列的代碼,顯然,所作的事情很是直白:線程拿到一個節點後將數據取出,並嘗試釋放節點。釋放節點是另外一個關鍵點,具體實現參看以下僞代碼:函數
thread_local vector<hp_t> free_list; void DeleteNode(node_t* t) { free_list.push_back(t); if (free_list.size() > R) FreeNode(); } void FreeNode() { vector<hp_t> hp_list; hp_list.reserve(N); // 獲取全部線程的 HP,如非空則保存到 hp_list 中。 for (int i = 0; i < N; ++i) { if (hp[i] == null) continue; hp_list.push_back(hp[i]); } std::sort(hp_list); vector<hp_t> not_free; not_free.reserve(free_list.size()); // 把當前線程的 free_list 遍歷遂一進行釋放。 for (int i = 0;i < free_list.size(); ++i) { if (std::binary_search(hp_list.begin(), hp_list.end(), free_list[i])) { // 某個線程持有當前節點,故不能刪除,仍是保留在隊列裏。 not_free.push_back(free_list[i]); continue; } // 確認沒有任何線程持有該節點,刪除之。 delete free_list[i]; } free_list.swap(not_free); }
看到這裏相信讀者對 Hazard Pointer 的原理已經大概瞭解了,那麼咱們來簡單總結一下上面的實現。學習
首先是效率問題,它夠快嗎?根據前面的僞代碼,顯然影響效率的關鍵點在FreeNode()
這個函數上,該函數有一個雙重循環,但還好第二重循環用了二分查找,所以刪除 R 個節點總的時間效率理論上是 O(R*logN),R 能夠設置, N 是線程數目,一般也不會太大,所以均攤下來應該還好?我只能說不知道,看使用場景吧,用無瑣通常有很高的效率需求,這裏加個這樣複雜度的處理是否會比加瑣更快呢?也說不許,實現上覆雜了是確定的,想用的話得好好測試測試看看劃不划得來。測試
其次是易用性,HP 釋放節點是累進制的,只有當一個線程積累到了必定數量的節點才批量進行釋放,而生產環境裏一般狀況複雜,會不會某個線程積累了幾個節點後,就再也不去隊列裏 pop 數據了呢?豈不是總有些節點不能釋放?內心有些疙瘩。。除此,現代操做系統裏線程建立銷燬其實很頻繁,某個線程若是要退出了,還得記得把本身頭上的節點釋放一下,也是件麻煩事。有人可能會以爲爲何刪除節點時要把節點放到隊列裏再刪?畫蛇添足!直接遍歷 HP 數組直到沒有線程持有該節點不就行了 --- 放到隊列裏實際上是爲效率,不然每 pop 一次就遍歷一遍 HP list,並且搞很差還要反覆等待某個線程釋放節點,均攤下來效率過低。
最後,還有一個問題,相信讀者忍了好久了,HP 數組那裏,各個線程怎麼 index 進去取出本身的 HP 呢? thread id 嗎?那這個數組不得很大很大很大?
關於 HP 數組的實現上,做者其實也看到了問題,提出能夠用 list 來管理 HP,由於不是每一個線程都必須固定分配一個 HP,事實上只有當該線程正在進行 pop 操做的時候它才須要,pop 完了立刻就能夠把 HP 還回去了,所以數組能夠用鏈表來替換,固然這個鏈表也得是 Lock free 的,但這個鏈表能夠不用考慮回收和釋放實現上容易多了,和我在本系列文章的第四篇裏提到的思路是一致的。
但這樣用 List 來代替數組在必定程度也增長了效率負擔,由於每一個線程取出 HP 變得更慢了(首先是很容易引發多個線程衝突,其次用到了 CAS 以及函數調用的開銷),固然具體有多少效率損失還得看使用場景,須要好好測量一下---寫無瑣代碼不能少作的事情。
無瑣編程很難,但這並不表明它們所以只能是理論遊戲,Maged Michael 的無瑣系列文章啓發了不少人,這其中也包括 c++ 裏的大腕 Andrei Alexandrescu,吶吶,看這裏。