本文為原創,轉載請註明:http://www.cnblogs.com/gistao/
背景
上一篇只是細緻的把原始碼分析了一遍,而原始碼背後的設計思想並沒有寫,設計思想往往是最重要的,沒有它,基本無法做整體性的優化或正確的使用,
但是根據結果反推原因是困難的,也極容易不到位,這裡‘磕磕絆絆’寫下自己的理解,另外對原始碼裡的‘問題’也寫出來。
簡單
除錯一個多執行緒程式是比較頭疼的,而使用atomic來編寫一個正確的多執行緒資料結構更是困難的,出了問題一般都是隨機問題,且等著復現看log吧,
所以簡單這個特性在設計裡應是第一位的。
AtomicHashmap的key只支援int,為什麼不像tbb的concurrent_hash_map一樣也支援自定義型別的key呢?它完全可以通過把現有的key定位為
純的狀態機,再設定一個欄位來儲存自定義的key,我想就是為了簡單,因為使用者可以自行通過hash演算法將自定義的key轉換為int來解決。這樣還
節省了一個指標空間佔用,夠用夠簡單。
28原則
這裡說的28原則是指80%的cpu在執行20%的程式碼。rehash是hashmap必不可失的功能,但它明顯不在20%程式碼之內的範疇。
所以,AtomicHashmap可以不支援傳統的rehash,一方面是atomic的能力限制,另一方面是rehash夠複雜效率夠低,但Facebook的工程師選擇了
讓80%的cpu執行要夠快,而剩餘的20%cpu稍低點也可以接受的思路。
AtomicHashmap的rehash類似於dequue的擴充策略,這會有兩個結論
- 當容量未滿時,這屬於80%的概率事件,依然可以O(1)
- 當容量滿時,這屬於20%的概率事件,依然可以O(2),O(3),O(4...)
簡單+28原則
AtomicHashmap的衝突解決策略是線性探測,線性探測會因為本次衝突而影響其他key的插入,而拉鍊法沒有這個問題。為什麼Facebook的工程師
選擇這個呢,我想首先衝突也是屬於20%概率事件,那麼程式碼效率稍差也是可以接受的,其次這個拉鍊其實就是個多生產者多消費者模式下的佇列,
參見boost的一個無鎖實現,比線性探測複雜多了,而線性探測也有個優點就是區域性性好。
O(1)變成O(N)
看個場景
step1:100個併發,每個併發做100次的隨機插入,AtomicHashmap的size設定為10w,總共插入14w資料。
step2:2個併發查詢,查詢的key不存在
step3:cpu idle降為0
如何解決
通過上一篇的原始碼分析,得出有三個懷疑點
- 要查的key發生過沖突,那麼查詢最好的情況是往後遍歷一個或者幾個(取決於hash演算法和容量大小)找到元素,
或者發現空元素,然後結束查詢;而最壞的情況(map裡空間全部被佔用)是遍歷查詢一遍,
當然這種可能性很小,取決於填充因子和插入的併發度 - 要查的key沒有插入過,那麼最好的情況是遇到第一個空元素結束查詢,最壞的情況是遍歷查詢一遍
- 要查的key已經被刪除了,這種情況同上
總結就是空間上空元素的分佈很重要,而分佈情況有三種
- 所有元素都被使用,沒有空元素
- 被使用元素集中分佈,相應的空元素也集中分佈
- 被使用元素和空元素相隔/均勻分佈
後兩種分佈主要取決於hash演算法,好的hash演算法能儘量保證每一bit變化的輸入反映在輸出上。
測試場景使用的hash演算法是通用的Murmurhash,此演算法效果是比較好的,在實際使用中並不會發生這兩種情況,
那麼問題就只有第一種分佈:沒有空元素
insertInternal(KeyT key_in, T&& value) {
......
if (isFull_.load(std::memory_order_acquire))
return false; //滿了,不讓再插入這個map了 ++numEntries_; //已插入的數量
if (numEntries_.readFast() >= maxEntries_) {
isFull_.store(true, std::memory_order_relaxed); //isfull設定為true
......
}
這是AtomicHashmap的插入邏輯:當插滿時,不允許插入。那應該不會出現沒有空元素的情況吧?no
maxEntries_為10w,填充因子為0.8,那麼元素的容量=12.5w(10/0.8),那麼空元素的容量為2.5w(12.5w-10w)。
可是為什麼空元素的容量為0呢?
numEntries_為thread_cache_int型別,這個類的大致思想是可以配置一個cache_size,那麼訪問這個numEntries_物件
的所有執行緒的區域性變數++到cache_size後才會同步給其他執行緒(即readFast可以獲取到)。
比如cache_size配置為1000,執行緒數為100,那麼理論上readFast的最大延遲同步為100*1000=10w。
10w遠大於空元素的容量2.5w,即有可能發生實際插入元素已經滿了(空元素容量為0),而isFull_依然為false。
遇到這種問題,解決思路是把容量調大。