一個用 C++ 實現的快速無鎖隊列

http://www.oschina.net/translate/a-fast-lock-free-queue-for-cpp?print


在進程間傳遞數據很煩人,真心煩人。一步做錯,數據就會損壞。(相較於其他讀寫方式)即使數據排列正確,也更易出錯。

一如既往的,有兩種方式處理這個問題:簡單的方式、麻煩的方式。

簡單的方式

使用你使用平臺提供的鎖(互斥、臨界區域,或等效)。這從概念上不難理解,使用上更簡單。你無需擔心排列問題,庫或OS會替你解決。使用鎖的唯一問題就是慢(這裏的“慢”是相對而言的:一般應用,它的速度足夠了)。

The catch

我一直都在尋找這樣一個音頻程序,這個音頻程序的回調(也就是說當設備需要更多的樣例來填補緩衝時程序便被調用)運行在另一個線程中。設計這樣音頻程序的目標是永遠也不要讓這個程序出現問題。--這就意味着你要儘可能的讓你的回調代碼以真實程序時間運行在一個常見的操作系統上。所以,系統計算速度越快越好,但是真正的難點在於如何確定所謂程序真是時間也就是起關鍵性作用的計算時間--也就是說,你代碼花費的時間也能是5ms也可能突然的變成300ms。

這意味着什麼?這麼說吧,就好像啓動器不能分配堆內存空間--這是因爲這個過程很可能關聯着一些系統任務的調度,而這些調度很可能需要一些無關緊要的時間去完成(比如,就好像你必須要話費一定的時間去等待網頁與磁盤之間進行的交換)。事實上,我們要儘量避免在內核上進行調用。然而,這還不是問題的關鍵。

問題的關鍵在於音頻程序的調用線程與主線程之間的同步問題。我們不能使用鎖,因爲這回有一些不必要的消費(考慮到回調函數的經常調用),這還會引起優先級倒置--你沒有去等着一個高優先級的音頻線程,而這個音頻線程正在等待一個關鍵性的部分通過後臺線程去釋放自己的空間,這就會使得音頻程序產生問題。

困難的方式

走進無鎖編程。

在競爭的情況下,無鎖編程是一種編寫線程安全代碼的方式,保證系統作爲一個整體推進。“無等待”編程進一步完善這點:代碼被設置,這樣不管對方在做什麼,每個線程總可以推進。這也避免了使用昂貴的鎖。爲什麼不是每個人都使用無鎖編程呢?嗯,它不那麼容易用對。任何情況下,不管每個線程在做什麼,你必須很小心,絕不能破壞數據。但更難的是順序相關的保證。考慮這個例子(a和b從0開始):

Thread A    Thread B
b = 42
a = 1
            print(a)
            print(b)

線程B可能會輸出的一組值是什麼呢?有可能是{0 0 },{1 42},{0 42}和{1 0}, {0 0}, {1 42}中的任何一組,{0 42}(其實是一個取決於時間的結果)可以說得通,但是{1 0}是怎麼出現的呢?這是因爲只要在線程裏出現,編譯器都允許在加載和存儲時(或者甚至在移除和構造時)進行重組以提高效率。但是當另一個線程開始與第一個交互時,重組就會變得很明顯了。

情況變得糟糕了,你可以強行使編譯器不對特定的讀寫進行重組,但是CPU也會允許(並且一直會)重組指令,只要指令在運行,內核中就會出現再一次相同的情況。

內存屏障

幸運的是,你可以通過內存屏障強制某些順序保證始終下調到CPU級別。不幸的是,要正確使用它們也很棘手。事實上,無所編碼的一個主要問題是很容易出現一些這樣的漏洞,只能通過特定線程操作的非確定性交錯的方式來重現。這意味着無鎖算法可能存在一個95%的時間工作正常而另外5%的時間會失敗的漏洞。或者它可能在開發者的CPU上總是正常工作,但在另一個CPU上偶爾失敗。

這就是無鎖編程通常被認爲很難的原因:你很容易得到一些看上去正確,但需要更多努力和思考來創造一些能保證在任何情況下都工作的事物。幸好,有一些工具能幫助驗證這些實現。其中一個工具叫Relacy:它的工作方式是,在各種可能的排列線程交織中運行(你寫的)單元測試,這實在太酷了。

回到內存屏障:在CPU的最底層,每個核都擁有自己的緩存,並且這些緩存必須彼此之間保持同步(一種最終統一)。這是由CPU內核之間的一種內部信息機制來完成的(這樣看來很慢?確實是的。所以這種機制經過高度優化,猜一猜它是怎麼實現的,就是更多的緩存!)。因爲兩個內核在同事執行代碼,所以會導致一些有意思的的內存加載/存儲順序(比如上面的例子)。內存屏障做的事情就是,在這個信息機制的頂層建立一些更強壯的順序確保機制。我發現的最強大的內存屏障是acquire and release(這裏翻譯沒啥意思,後面也會用這個名次)。“release” 內存屏障告訴CPU,如果屏障後的寫操作對其他核變爲可見,那麼屏障前的寫操作也必須保持可見。這種轉變是在其他核讀取數據(這些數據是“寫屏障”之後寫入的)之後,執行讀屏障的時候進行的。換句話說,如果一個線程B可以看見另一個線程A在一個寫屏障之後寫入的新值,那麼在執行一個讀屏障之後(在線程B上),所有在寫屏障之前A線程上進行的寫操作都會對B線程可見。一個不錯的功能: “acquire and release” 語義,類似於x86上的停止操作指令, 就是每個寫操作都隱含release語義,每個讀操作都隱含acquire語義。但是你仍然需要手動添加屏障,因爲 “acquire and release” 語義不允許編譯器重新排列內存加載順序(並且它也會生成在其他處理器架構上同樣正確的彙編代碼,即使這些處理器沒有強壯的內存排序模型)。

如果這令你很困惑,好吧,我承認確實很困惑。但是不要放棄希望,因爲這裏有一些資源,它們很好地解釋了這個問題。從Jeff Preshing's very helpful articles on the subject這篇文章開始是不錯的,這個頁面上還有一些其他鏈接的列表: this answer on Stack Overflow

一個等待釋放的單一的生產者,單一的消費者隊列

自從我顯然發現不可抗拒的突然轉向時(譯者:且這麼理解),我,當然,建立了自己的無鎖數據結構,我爲了一個單一的生產者、單一的消費者體系結構盡力想求得一個等待釋放的隊列(意味着只有兩個線程參與)。如果你立刻需要一個安全使用多線程的數據結構,你需要找到另一種實現(MSVC++帶有一個)。我選擇限制是因爲它很大程度上操作要比釋放全部併發線程簡單,以及這就是我所有我需要的。

我看了一些目前的書籍(特別是liblfds),但是我不太滿意內存管理(我想要一個不分配任何內存給所有關鍵線程的隊列,使得它適合實時編程),所以,我忽略了關於只做無鎖編程的大量建議除非你已經是這一領域的專家(如何成爲一個這樣的專家呢?),並能夠成功地實現一個隊列!

設計如下:

一個連續的環形緩衝區(buffer)被用來儲存隊列中的元素。這樣允許內存被分配在最前面,並且可能提供更好的緩存使用。我把這個緩衝區叫做“區塊”(block)。

爲了使得隊列能夠動態增長,並且當區塊變的太小時(總之不是無鎖的)不需要複製所有已經存在的元素到新的區塊裏,許多的區塊(有獨自的大小)會被鏈接在一起形成環形相關聯的列表。這樣就形成了隊列的隊列。

元素正在被插入的區塊叫做“尾區塊”。元素正在被消耗的區塊叫做“頭區塊”。相同地,在每一個區塊裏有一個“頭"索引和一個“尾”索引。頭索引指示了下一個將被讀取的滿的位置,尾索引指示了下一個將被插入的空的位置。如果這兩個索引是相等的,那麼這個區塊是空的(一個位置會被清空當隊列是滿的,這樣爲了避免當一個滿的區塊和一個空的區塊都有相同頭和尾索引時產生的不明確性)。

爲了保持一致性,兩個線程操作同一數據結構,我們可以利用生產線程在隊列中總是運行在一個方向上的實際情況和同樣可以用來說明的消費者線程,這意味着,即使一個變量值在給定的線程中是過時的,我們也知道它可能的所在的範圍。例如,當我們從一個阻塞出隊,我們可以檢查尾部索引的值(由其它線程擁有)來比較緊靠着前面的索引(該線程自己出隊,並因此總是最新的),我們可以爲尾部索引從CPU緩存獲得一箇舊值,其後入隊線程可以增加更多元素,但是,我們知道尾部絕不會倒退——更多元素會被增加,只要尾部不等於前面我們檢查過的,保證至少有一個元素出隊。

使用這些擔保,和一些內存分界線去阻止像被增加在元素前面(或者被認爲是前加)事實上是被增加到隊列尾部,設計一個簡單算法來安全地入隊和出隊的元素在所有可能的線程交織下是可能的,這是僞代碼:

# Enqueue
If room in tail block, add to tail
Else check next block
    If next block is not the head block, enqueue on next block
    Else create a new block and enqueue there
    Advance tail to the block we just enqueued to

# Dequeue
Remember where the tail block is
If the front block has an element in it, dequeue it
Else
    If front block was the tail block when we entered the function, return false
    Else advance to next block and dequeue the item there

單個固定尺寸的塊的入隊和出隊算法是比較簡單的:

1 #向一個塊的入隊(假設我們已經檢查了一個塊中有空間)
2 複製/移動元素進入塊的連續存儲空間
3 隊尾下標增長(需要重新設置隊尾)
4  
5 #從一個塊的出隊(假設我們已經檢查了非空)
6 從一個塊的連續存儲空間複製/移動元素到一個輸出參數
7 隊尾下標增長(需要重新設置隊尾)
顯然,這掩蓋了一些東西(比如可能存在的內存屏障),但是如果你希望檢查底層錯誤細節的話, 實際代碼也不會非常複雜。 

再思考這個數據結構,它對於區分消費者線程或生產者線程所擁有的變量(例如:寫入獨佔變量)是非常有幫助的。對於一個給定的線程,他所擁有的變量永遠不會過時。一個線程擁有的變量被另一個線程讀取到的可能只是一箇舊值,但是通過小心的使用內存屏障,但我們從一個並不擁有這個變量的線程讀取時,我們能夠保證其餘的數據內容至少是新的。 

允許隊列可能被任意線程創建或銷燬(兩個相互獨立的生產者和消費者線程),一個完整的內存屏障(memory_order_seq_cst)用於構造函數的結尾和析構函數的開始; 這樣可以有效的迫使所有CPU內核有效同步。顯然,生產者和消費者必須在 析構函數可以安全調用之前已經停止使用隊列。


給我代碼

如果沒有可靠的(已被測試的)實現,設計又有什麼用呢?:-)

我已經 在GitHub發佈了我的實現。 自由的fork它吧!它由兩個頭部組成,一個是給隊列的,還有一個取決於是否包含一些輔助參數。

它具有幾個優異的特性:

  • 與 C++11兼容 (支持移動對象而不是做拷貝)
  • 完全通用 (任何類型的模板化容器) -- 就像std::queue,你從不需要自己給元素分配內存 (這將你從爲了管理正在排隊的元素而去寫鎖無關的內存管理單元的麻煩中解脫出來)
  • 以連續的塊預先分配內存
  • 提供 atry_enqueue方法,該方法保證不去分配內存 (隊列以初始容量起動)
  • 也提供了一個enqueue方法,該方法能夠根據需要動態的增長隊列的大小
  • 不採用比較-交換循環;這意味着 enqueue和dequeue是O(1)複雜度 (不計算內存分配)
  • 對於x86設備, 內存屏障編譯爲空指令,這意味着enqueue與dequeue僅僅只是簡單的loads和stores序列 (以及 branches)
  • 在 MSVC2010+ 和 GCC 4.7+下編譯 (而且應該工作於任何支持 C++11 的編譯器)


應注意的是,此代碼只能工作於能處理對齊的整數和原生指針長度的負載/存儲原子的CPU;幸運的是,這包括所有的現代處理器(包括 ARM,x86 / x86-64,和PowerPC)。它不能工作於 DEC Alpha(這玩意內存排序能力保證最弱)。

我發佈的代碼和算法遵循簡化的BSD授權協議。你需要自己承擔使用風險;特別是,無鎖編程是一個專利的雷區,這代碼很可能違反了專利(我還沒查驗)。需要提出的是,我是自己胡亂寫出來的算法和實現,與任何現有的無鎖隊列無關。


性能測試和無誤較正

除了折騰在相當長的一段時間的設計,我(X86)測試了一個簡單的穩定性測試使用數十億隨機操作的算法。 當然,這有助於鼓舞信心,但不能證明什麼的正確性。 爲了確保它是正確的,我的測試也使用了Relacy,跑了一個簡單的測試來測試所有可能的交錯。沒有發現錯誤;但是,事實證明這個簡單的測試是不全面的,因爲通過使用一組不同的隨機運行,我發現了一個錯誤(當然我最後修正了這些)。

我只在x86-64架構的機器上測試此隊列,內存佔用是相當寬裕(少)的。如有人樂意在其他架構機器上測試這些代碼,告訴我吧。快速穩定性的測試代碼我放在了這兒 。

在性能方面,它是非常快的,真的非常的塊。在我的測試中,能夠達到約每秒大於12Million組的併發入隊/出隊的操作(如果隊列中沒有數據出隊線程獲取數據之前必須等待入隊線程)。雖然在我實現我的隊列之後,我發現另一個發佈在 Intel的網站上的單消費者/單生產者模板隊列(作者是Relacy);他實現的隊列速度大致是我的兩倍,但是他並沒有實現我所實現的全部功能,並且他僅工作在X86平臺(這種條件下,“兩倍快”意味着這兩種不同的入隊/出隊實現在時間上相差非常小)。 

更新於16天前 

我花了一下時間修正我的實驗,分析和優化代碼,使用 Dmitry的單生產者/單消費者自由鎖隊列(發佈在 Intel網站)作爲比較參照。目前我的實現相對更快一些,特別是涉及到多元素入隊的時候(我的實現使用連續的塊替代分開的元素鏈接方式)。注意不同的編譯器會給出不同的結果,甚至相同的的編譯器在不同的硬件平臺上也顯著的表現出速度有所不同。64位的版本通常比32位版本的快。因爲某些原因,我的隊列實現在Linode上,使用GCC編譯器會更快。這裏是完整測試結果:

001 32-bit, MSVC2010, on AMD C-50 @ 1GHz
002 ------------------------------------
003                   |        Min        |        Max        |        Avg
004 Benchmark         |   RWQ   |  SPSC   |   RWQ   |  SPSC   |   RWQ   |  SPSC   | Mult
005 ------------------+---------+---------+---------+---------+---------+---------+------
006 Raw add           | 0.0039s | 0.0268s | 0.0040s | 0.0271s | 0.0040s | 0.0270s | 6.8x
007 Raw remove        | 0.0015s | 0.0017s | 0.0015s | 0.0018s | 0.0015s | 0.0017s | 1.2x
008 Raw empty remove  | 0.0048s | 0.0027s | 0.0049s | 0.0027s | 0.0048s | 0.0027s | 0.6x
009 Single-threaded   | 0.0181s | 0.0172s | 0.0183s | 0.0173s | 0.0182s | 0.0173s | 0.9x
010 Mostly add        | 0.0243s | 0.0326s | 0.0245s | 0.0329s | 0.0244s | 0.0327s | 1.3x
011 Mostly remove     | 0.0240s | 0.0274s | 0.0242s | 0.0277s | 0.0241s | 0.0276s | 1.1x
012 Heavy concurrent  | 0.0164s | 0.0309s | 0.0349s | 0.0352s | 0.0236s | 0.0334s | 1.4x
013 Random concurrent | 0.1488s | 0.1509s | 0.1500s | 0.1522s | 0.1496s | 0.1517s | 1.0x
014  
015 Average ops/s:
016     ReaderWriterQueue: 23.45 million
017     SPSC queue:        28.10 million
018  
019 64-bit, MSVC2010, on AMD C-50 @ 1GHz
020 ------------------------------------
021                   |        Min        |        Max        |        Avg
022 Benchmark         |   RWQ   |  SPSC   |   RWQ   |  SPSC   |   RWQ   |  SPSC   | Mult
023 ------------------+---------+---------+---------+---------+---------+---------+------
024 Raw add           | 0.0022s | 0.0210s | 0.0022s | 0.0211s | 0.0022s | 0.0211s | 9.6x
025 Raw remove        | 0.0011s | 0.0022s | 0.0011s | 0.0023s | 0.0011s | 0.0022s | 2.0x
026 Raw empty remove  | 0.0039s | 0.0024s | 0.0039s | 0.0024s | 0.0039s | 0.0024s | 0.6x
027 Single-threaded   | 0.0060s | 0.0054s | 0.0061s | 0.0054s | 0.0061s | 0.0054s | 0.9x
028 Mostly add        | 0.0080s | 0.0259s | 0.0081s | 0.0263s | 0.0080s | 0.0261s | 3.3x
029 Mostly remove     | 0.0092s | 0.0109s | 0.0093s | 0.0110s | 0.0093s | 0.0109s | 1.2x
030 Heavy concurrent  | 0.0150s | 0.0175s | 0.0181s | 0.0200s | 0.0165s | 0.0190s | 1.2x
031 Random concurrent | 0.0367s | 0.0349s | 0.0369s | 0.0352s | 0.0368s | 0.0350s | 1.0x
032  
033 Average ops/s:
034     ReaderWriterQueue: 34.90 million
035     SPSC queue:        32.50 million
036  
037 32-bit, MSVC2010, on Intel Core 2 Duo T6500 @ 2.1GHz
038 ----------------------------------------------------
039                   |        Min        |        Max        |        Avg
040 Benchmark         |   RWQ   |  SPSC   |   RWQ   |  SPSC   |   RWQ   |  SPSC   | Mult
041 ------------------+---------+---------+---------+---------+---------+---------+------
042 Raw add           | 0.0011s | 0.0097s | 0.0011s | 0.0099s | 0.0011s | 0.0098s | 9.2x
043 Raw remove        | 0.0005s | 0.0006s | 0.0005s | 0.0006s | 0.0005s | 0.0006s | 1.1x
044 Raw empty remove  | 0.0018s | 0.0011s | 0.0019s | 0.0011s | 0.0018s | 0.0011s | 0.6x
045 Single-threaded   | 0.0047s | 0.0040s | 0.0047s | 0.0040s | 0.0047s | 0.0040s | 0.9x
046 Mostly add        | 0.0052s | 0.0114s | 0.0053s | 0.0116s | 0.0053s | 0.0115s | 2.2x
047 Mostly remove     | 0.0055s | 0.0067s | 0.0056s | 0.0068s | 0.0055s | 0.0068s | 1.2x
048 Heavy concurrent  | 0.0044s | 0.0089s | 0.0075s | 0.0128s | 0.0066s | 0.0107s | 1.6x
049 Random concurrent | 0.0294s | 0.0306s | 0.0295s | 0.0312s | 0.0294s | 0.0310s | 1.1x
050  
051 Average ops/s:
052     ReaderWriterQueue: 71.18 million
053     SPSC queue:        61.02 million
054  
055 64-bit, MSVC2010, on Intel Core 2 Duo T6500 @ 2.1GHz
056 ----------------------------------------------------
057                   |        Min        |        Max        |        Avg
058 Benchmark         |   RWQ   |  SPSC   |   RWQ   |  SPSC   |   RWQ   |  SPSC   | Mult
059 ------------------+---------+---------+---------+---------+---------+---------+------
060 Raw add           | 0.0007s | 0.0097s | 0.0007s | 0.0100s | 0.0007s | 0.0099s | 13.6x
061 Raw remove        | 0.0004s | 0.0015s | 0.0004s | 0.0020s | 0.0004s | 0.0018s | 4.6x
062 Raw empty remove  | 0.0014s | 0.0010s | 0.0014s | 0.0010s | 0.0014s | 0.0010s | 0.7x
063 Single-threaded   | 0.0024s | 0.0022s | 0.0024s | 0.0022s | 0.0024s | 0.0022s | 0.9x
064 Mostly add        | 0.0031s | 0.0112s | 0.0031s | 0.0115s | 0.0031s | 0.0114s | 3.7x
065 Mostly remove     | 0.0033s | 0.0041s | 0.0033s | 0.0041s | 0.0033s | 0.0041s | 1.2x
066 Heavy concurrent  | 0.0042s | 0.0035s | 0.0067s | 0.0039s | 0.0054s | 0.0038s | 0.7x
067 Random concurrent | 0.0142s | 0.0141s | 0.0145s | 0.0144s | 0.0143s | 0.0142s | 1.0x
068  
069 Average ops/s:
070     ReaderWriterQueue: 101.21 million
071     SPSC queue:        71.42 million
072  
073 32-bit, Intel ICC 13, on Intel Core 2 Duo T6500 @ 2.1GHz
074 --------------------------------------------------------
075                   |        Min        |        Max        |        Avg
076 Benchmark         |   RWQ   |  SPSC   |   RWQ   |  SPSC   |   RWQ   |  SPSC   | Mult
077 ------------------+---------+---------+---------+---------+---------+---------+------
078 Raw add           | 0.0014s | 0.0095s | 0.0014s | 0.0097s | 0.0014s | 0.0096s | 6.8x
079 Raw remove        | 0.0007s | 0.0006s | 0.0007s | 0.0007s | 0.0007s | 0.0006s | 0.9x
080 Raw empty remove  | 0.0028s | 0.0013s | 0.0028s | 0.0018s | 0.0028s | 0.0015s | 0.5x
081 Single-threaded   | 0.0039s | 0.0033s | 0.0039s | 0.0033s | 0.0039s | 0.0033s | 0.8x
082 Mostly add        | 0.0049s | 0.0113s | 0.0050s | 0.0116s | 0.0050s | 0.0115s | 2.3x
083 Mostly remove     | 0.0051s | 0.0061s | 0.0051s | 0.0062s | 0.0051s | 0.0061s | 1.2x
084 Heavy concurrent  | 0.0066s | 0.0036s | 0.0084s | 0.0039s | 0.0076s | 0.0038s | 0.5x
085 Random concurrent | 0.0291s | 0.0282s | 0.0294s | 0.0287s | 0.0292s | 0.0286s | 1.0x
086  
087 Average ops/s:
088     ReaderWriterQueue: 55.65 million
089     SPSC queue:        63.72 million
090  
091 64-bit, Intel ICC 13, on Intel Core 2 Duo T6500 @ 2.1GHz
092 --------------------------------------------------------
093                   |        Min        |        Max        |        Avg
094 Benchmark         |   RWQ   |  SPSC   |   RWQ   |  SPSC   |   RWQ   |  SPSC   | Mult
095 ------------------+---------+---------+---------+---------+---------+---------+------
096 Raw add           | 0.0010s | 0.0099s | 0.0010s | 0.0100s | 0.0010s | 0.0099s | 9.8x
097 Raw remove        | 0.0006s | 0.0015s | 0.0006s | 0.0018s | 0.0006s | 0.0017s | 2.7x
098 Raw empty remove  | 0.0024s | 0.0016s | 0.0024s | 0.0016s | 0.0024s | 0.0016s | 0.7x
099 Single-threaded   | 0.0026s | 0.0023s | 0.0026s | 0.0023s | 0.0026s | 0.0023s | 0.9x
100 Mostly add        | 0.0032s | 0.0114s | 0.0032s | 0.0118s | 0.0032s | 0.0116s | 3.6x
101 Mostly remove     | 0.0037s | 0.0042s | 0.0037s | 0.0044s | 0.0037s | 0.0044s | 1.2x
102 Heavy concurrent  | 0.0060s | 0.0092s | 0.0088s | 0.0096s | 0.0077s | 0.0095s | 1.2x
103 Random concurrent | 0.0168s | 0.0166s | 0.0168s | 0.0168s | 0.0168s | 0.0167s | 1.0x
104  
105 Average ops/s:
106     ReaderWriterQueue: 68.45 million
107     SPSC queue:        50.75 million
108  
109 64-bit, GCC 4.7.2, on Linode 1GB virtual machine (Intel Xeon L5520 @ 2.27GHz)
110 -----------------------------------------------------------------------------
111                   |        Min        |        Max        |        Avg
112 Benchmark         |   RWQ   |  SPSC   |   RWQ   |  SPSC   |   RWQ   |  SPSC   | Mult
113 ------------------+---------+---------+---------+---------+---------+---------+------
114 Raw add           | 0.0004s | 0.0055s | 0.0005s | 0.0055s | 0.0005s | 0.0055s | 12.1x
115 Raw remove        | 0.0004s | 0.0030s | 0.0004s | 0.0030s | 0.0004s | 0.0030s | 8.4x
116 Raw empty remove  | 0.0009s | 0.0060s | 0.0010s | 0.0061s | 0.0009s | 0.0060s | 6.4x
117 Single-threaded   | 0.0034s | 0.0052s | 0.0034s | 0.0052s | 0.0034s | 0.0052s | 1.5x
118 Mostly add        | 0.0042s | 0.0096s | 0.0042s | 0.0106s | 0.0042s | 0.0103s | 2.5x
119 Mostly remove     | 0.0042s | 0.0057s | 0.0042s | 0.0058s | 0.0042s | 0.0058s | 1.4x
120 Heavy concurrent  | 0.0030s | 0.0164s | 0.0036s | 0.0216s | 0.0032s | 0.0188s | 5.8x
121 Random concurrent | 0.0256s | 0.0282s | 0.0257s | 0.0290s | 0.0257s | 0.0287s | 1.1x
122  
123 Average ops/s:
124     ReaderWriterQueue: 137.88 million
125     SPSC queue:        24.34 million

簡而言之,我實現的隊列令人驚奇的快,而且確實還包括任何它自身數據結構所佔的開銷。

測試代碼參見 這裏(用最高優化選項編譯運行)。

更新 
2013-5-20的更新:我改變了隊列內存的預分配基準,並增加了一個針對 Facebook's folly::ProducerConsumerQueue(這個是有點快,但不能根據需要增加)的比較。下面是比較結果:
64-bit, GCC 4.7.2, on Linode 1GB virtual machine (Intel Xeon L5520 @ 2.27GHz)
-----------------------------------------------------------------------------
                  |-----------  Min ------------|------------ Max ------------|------------ Avg ------------|
Benchmark         |   RWQ   |  SPSC   |  Folly  |   RWQ   |  SPSC   |  Folly  |   RWQ   |  SPSC   |  Folly  | xSPSC | xFolly
------------------+---------+---------+---------+---------+---------+---------+---------+---------+---------+-------+-------
Raw add           | 0.0003s | 0.0018s | 0.0003s | 0.0003s | 0.0018s | 0.0003s | 0.0003s | 0.0018s | 0.0003s | 5.52x | 0.96x
Raw remove        | 0.0003s | 0.0030s | 0.0004s | 0.0003s | 0.0030s | 0.0004s | 0.0003s | 0.0030s | 0.0004s | 8.58x | 1.28x
Raw empty remove  | 0.0009s | 0.0061s | 0.0006s | 0.0010s | 0.0061s | 0.0006s | 0.0010s | 0.0061s | 0.0006s | 6.40x | 0.61x
Single-threaded   | 0.0034s | 0.0053s | 0.0033s | 0.0034s | 0.0053s | 0.0033s | 0.0034s | 0.0053s | 0.0033s | 1.54x | 0.97x
Mostly add        | 0.0042s | 0.0046s | 0.0042s | 0.0043s | 0.0046s | 0.0042s | 0.0042s | 0.0046s | 0.0042s | 1.09x | 0.99x
Mostly remove     | 0.0042s | 0.0049s | 0.0043s | 0.0042s | 0.0051s | 0.0043s | 0.0042s | 0.0050s | 0.0043s | 1.20x | 1.02x
Heavy concurrent  | 0.0025s | 0.0100s | 0.0024s | 0.0028s | 0.0101s | 0.0026s | 0.0026s | 0.0100s | 0.0025s | 3.88x | 0.97x
Random concurrent | 0.0265s | 0.0268s | 0.0262s | 0.0273s | 0.0287s | 0.0284s | 0.0269s | 0.0280s | 0.0271s | 1.04x | 1.01x

Average ops/s:
    ReaderWriterQueue: 142.60 million
    SPSC queue:        33.86 million
    Folly queue:       181.16 million

無鎖編程的未來

我認爲多線程代碼會變得更加普遍,越來越多的庫將會利用無鎖編程的速度優勢(同時從應用程序開發者身上將暴力的、傾向出錯的勇氣轉移出去)。不過,我想知道,到底多核心共享內存架構會走多遠——對高度並行的CPU來說,關於高速緩存一致性協議和記憶障礙的說明似乎並不是成正比的好(性能明智)。可被本地線程寫的不可變共享內存在未來是可能的。聽起來像是函數式編程的工作!

發佈了5 篇原創文章 · 獲贊 1 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章