本文為原創,轉載請註明: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。

遇到這種問題,解決思路是把容量調大。