1. 程式人生 > >一種高效無鎖記憶體佇列的實現

一種高效無鎖記憶體佇列的實現

Disruptor是LMAX公司開源的一個高效的記憶體無鎖佇列。這兩天看了一下相關的設計文件和部落格,下面嘗試進行一下總結。

第一部分。引子
談到併發程式設計,有幾個概念是避免不了的。

1.鎖:鎖是用來做併發最簡單的方式,當然其代價也是最高的。核心態的鎖的時候需要作業系統進行一次上下文切換,等待鎖的執行緒會被掛起直至鎖釋放。在上下文切換的時候,cpu之前快取的指令和資料都將失效,對效能有很大的損失。使用者態的鎖雖然避免了這些問題,但是其實它們只是在沒有真實的競爭時才有效。下面是一個計數實驗中不加鎖、使用鎖、使用CAS及定義volatile變數之間的效能對比。

2. CAS: CAS的涵義不多介紹了。使用CAS時不像上鎖那樣需要一次上下文切換,但是也需要處理器鎖住它的指令流水線來保證原子性,並且還要加上Memory Barrier來保證其結果可見。

3. Memory Barrier: 大家都知道現代CPU是亂序執行的,也就是程式順序與實際的執行順序很可能是不一致的。在單執行緒執行時這不是個問題,但是在多執行緒環境下這種亂序就可能會對執行結果產生很大的影響了。memory barrier提供了一種控制程式執行順序的手段, 關於其更多介紹,可以參考 http://en.wikipedia.org/wiki/Memory_barrier

4. Cache Line:cache line解釋起來其實很簡單,就是CPU在做快取的時候有個最小快取單元,在同一個單元內的資料被同時被載入到快取中,充分利用 cache line可以大大降低資料讀寫的延遲,錯誤利用cache line也會導致快取不同替換,反覆失效。

好,接下來談一談設計併發記憶體佇列時需要考慮的問題。一就是資料結構的問題,是選用定長的陣列還是可變的連結串列,二是併發控問題,是使用鎖還是CAS操作,是使用粗粒度的一把鎖還是將佇列的頭、尾、和容量三個變數分開控制,即使分開,能不能避免它們落入同一個Cache line中呢。
我們再回過頭來思考一下佇列的使用場景。通常我們的處理會形成一條流水線或者圖結構,佇列被用來作為這些流程中間的銜接表示它們之間的依賴關係,同時起到一個緩衝的作用。但是使用佇列並不是沒有代價的,實際上資料的入隊和出隊都是很耗時的,尤其在效能要求極高的場景中,這種消耗更顯得奢侈。如果這種依賴能夠不通過在各個流程之間放一個佇列來表示那就好啦!
第二部分 正文


現在開始來介紹我們的Disruptor啦,有了前面這麼多的鋪墊,我想可以直入主題了。接下來我們就從佇列的三種基本問題來細細分析下disruptor吧。

1.列隊中的元素如何儲存?
Disruptor的中心資料結構是一個基於定長陣列的環形佇列,如圖1。
在陣列建立時可以預先分配好空間,插入新元素時只要將新元素資料拷貝到已經分配好的記憶體中即可。對陣列的元素訪問對CPU cache 是非常友好的。關於陣列的大小選擇有一個講究,大家都知道環形佇列中會用到取餘操作, 在大部分處理器上,取餘操作並不高效。因此可以將陣列大小設定為2的指數倍,這樣計算餘數只需要通過位操作 index & ( size -1 )就能夠得到實際的index。
Disruptor對外只有一個變數,那就是隊尾元素的下標:cursor,這也避免了對head/tail這兩個變數的操作和協同。生產者和消費者對disruptor的訪問分別需要通過producer barrier和consumer barrier來協調。關於這兩個barrier是啥,後面會介紹。
ring buffer
圖1. RingBuffer,當前的隊尾元素位置為18

2.生產者如何向佇列中插入元素?
生產者插入元素分為兩個步驟,第一步申請一個空的slot, 每個slot只會被一個生產者佔用,申請到空的slot的生產者將新元素的資料拷貝到該slot;第二步是釋出,釋出之後,新元素才能為消費者所見。如果只有一個生產者,第一步申請操作無需同步即可完成。如果有多個生產者,那麼會有一個變數:claimSequence來記錄申請位置,申請操作需要通過CAS來同步,例如圖二中,如果兩個生產者都想申請第19號slot, 則它們會同時執行CAS(&claimSequence, 18, 19),執行成功的人得到該slot,另一個則需要繼續申請下一個可用的slot。在disruptor中,釋出成功的順序與申請的順序是嚴格保持一致的,在實現上,釋出事件實際上就是修改cursor的值,操作等價於CAS(&cursor, myslot-1, myslot),從此操作也可以看出,釋出執行成功的順序必定是slot, slot 1, slot 2 ….嚴格有序的。另外,為了防止生產者生產過快,在環形佇列中覆蓋消費者的資料,生產者要對消費者的消費情況進行跟蹤,實現上就是去讀取一下每個消費者當前的消費位置。例如一個環形佇列的大小是8,有兩個消費者的分別消費到第13和14號元素,那麼生產者生產的新元素是不能超過20的。插入元素的過程圖示如下:
publisher1
圖2. RingBuffer當前的隊尾位置序號為18.生產者提出申請。

圖3. 生產者申請得到第19號位置,並且19號位置是獨佔的,可以寫入生產元素。此時19號元素對消費者是不可見的。

圖4,生產者成功寫入19號位置後,將cursor修改為19,從而完成釋出,之後消費者可以消費19號元素。

3.消費者如何獲知有新的元素進來了?
消費者需要等待有新元素進入方能繼續消費,也就是說cursor大於自己當前的消費位置。等待策略有多種。可以選擇sleep wait, busy spin等等,在使用disruptor時,可以根據場景選擇不同的等待策略。

4.批量
如果消費者發現cursor相比其最後的一次消費位置前進了不止一個位置,它就可以選擇批量消費這區段的元素,而不是一次一個的向前推進。這種做法在提高吞吐量的同時還可以使系統的延遲更加平滑。

5.依賴圖
前面也提過,在傳統的系統中,通常使用佇列來表示多個處理流程之間的依賴,並且一步依賴就需要多新增一個佇列。在Disruptor中,由於生產者和消費者是分開考慮和控制的,因此有可能能夠通過一個核心的環形佇列來表示全部的依賴關係,可以大大提高吞吐,降低延遲。當然,要達到這個目的,還需要使用者細心地去設計。下面舉一個簡單的例子來說明如何使用disruptor來表示依賴關係。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 /** * 場景描述:生產者p1生產出來的資料需要經過消費者ep1和ep2的處理,然後傳遞給消費者ep3 * *            ----- *     ----->| EP1 |------ *    |       -----       | *    |                   v *  ----                ----- * | P1 |              | EP3 | *  ----                ----- *    |                   ^ *    |       -----       | *     ----->| EP2 |------ *            ----- * * * 基於佇列的解決方案 * ============ *                 take       put *     put    ====      -----      ====   take *     ----->| Q1 |<---| EP1 |--->| Q3 |<------ *    |       ====      -----      ====        | *    |                                        | *  ----      ====      -----      ====      ----- * | P1 |--->| Q2 |<---| EP2 |--->| Q4 |<---| EP3 | *  ----      ====      -----      ====      ----- * * 使用Disruptor的解決方案: * 以一個RingBuffer為中心,生產者p1生產事件寫到ringbuffer中, * 消費者ep1和ep2僅需要根據隊尾位置來進行判斷是否有可消費事件即可, * 消費者ep3則需要根據消費者ep1和ep2的位置來判斷是否有可消費事件。生產者需要跟蹤ep3的位置,防止覆蓋未消費事件。 * ========== *                    track to prevent wrap *               ------------------------------- *              |                               | *              |                               v *  ----      ====                 =====      ----- * | P1 |--->| RB |<--------------| SB2 |<---| EP3 | *  ----      ====                 =====      ----- *      claim   ^  get               |   waitFor *              |                    | *            =====      -----       | *           | SB1 |<---| EP1 |<----- *            =====      -----       | *              ^                    | *              |        -----       | *               -------| EP2 |<----- *             waitFor   ----- */

第三部分 結束語
disruptor本身是用java寫的,但是筆者認為在c 中更能體現其優點,自己也山寨了一個c 版本。在一個生產者和一個消費者的場景中測試表明,無鎖佇列相比有鎖佇列,qps有大約10倍的提升,latency更是有幾百倍的提升。不管怎麼樣,現在大家都漸漸都這麼一個意識了:鎖是效能殺手。所以這些無鎖的資料結構和演算法,可以嘗試借鑑來使用在合適的場景中。

轉載自:http://www.searchtb.com/2012/10/introduction_to_disruptor.html

相關推薦

高效記憶體佇列實現

Disruptor是LMAX公司開源的一個高效的記憶體無鎖佇列。這兩天看了一下相關的設計文件和部落格,下面嘗試進行一下總結。 第一部分。引子 談到併發程式設計,有幾個概念是避免不了的。 1.鎖:鎖是用來做併發最簡單的方式,當然其代價也是最高的。核心態的鎖的時候需要作業

【轉載】環形佇列高效實現

1.環形佇列是什麼  佇列是一種常用的資料結構,這種結構保證了資料是按照“先進先出”的原則進行操作的,即最先進去的元素也是最先出來的元素.環形佇列是一種特殊的佇列結構,保證了元素也是先進先出的,但與一般佇列的區別是,他們是環形的,即佇列頭部的上個元素是佇列尾部,通常是容

環形佇列高效實現

1.環形佇列是什麼  佇列是一種常用的資料結構,這種結構保證了資料是按照“先進先出”的原則進行操作的,即最先進去的元素也是最先出來的元素.環形佇列是一種特殊的佇列結構,保證了元素也是先進先出的,但與一般佇列的區別是,他們是環形的,即佇列頭部的上個元素是佇列尾部,通常是容

高效雙端佇列(連結串列)實現方式

本文將介紹一種高效的雙端佇列(連結串列),包括資料結構、新建佇列、插入頭結點、插入尾節點、刪除頭結點等,所有操作的時間複雜度均為O(1)。 為了方便理解,部分函式前面均配了一幅圖作為介紹。 首先,資料結構定義如下: typedef struct node_s {

c++11記憶體模型以及引用計數棧的實現

c++11提供了6中記憶體模型: memory_order_seq_cst(原子操作預設模型) memory_order_relaxed (沒有順序性的要求 memory_order_release memory_order_acquire memory_order

併發環形佇列實現

前面在《Linux核心資料結構kfifo詳解》一文中詳細解析了 Linux  核心併發無鎖環形佇列kfifo的原理和實現,kfifo鬼斧神工,博大精深,讓人歎為觀止,但遺憾的是kfifo為核心提供服務,並未開放出來。劍不試則利鈍暗,弓不試則勁撓誣,鷹不試則巧拙惑,馬不試則良駑

SpringBank 開發日誌 簡單的攔截器設計實現

exp bst 一個 pin factory span 之前 system request 當交易由Action進入Service之前,需要根據不同的Service實際負責業務的不同,真正執行Service的業務邏輯之前,做一些檢查工作。這樣的攔截器應該是基於配置的,與Se

數據庫查詢速度慢了?是否是因為死了?解決死的方法。

-- 檢索 blog kill _id exe 進行 xxx 通過 --查詢哪些表被死鎖 select request_session_id spid,OBJECT_NAME(resource_associated_entity_id) tableName from sy

基於 Numpy 的 TF-IDF 實現報告

常用 離線 數據結構與算法分析 dex 參考文獻 代碼 運行 數組 步驟 一種基於 Numpy 的 TF-IDF 實現報告 摘要 本文使用了一種 state-of-the-art 的矩陣表示方法來計算每個詞在每篇文章上的 TF-IDF 權重(特征)。本文還將介紹基於 TF-

高效的QPS統計方法

循環數組 eset 指數 yun return hashmap await spa 全面 一、概述 對QPS的統計,通常是對一秒內各線程通過數據處理鏈中某一個切入點的次數進行累加計數。且不論采用何種方式都繞不開鎖,那如何結合QPS統計的場景,減少線程之間對鎖的競爭,是各實現

[論文學習]An Effective Approach for Mining Mobile User Habits:高效挖掘移動使用者習慣的方法

原文: Cao H, Bao T, Yang Q, et al. An effective approach for mining mobile user habits[C]//Proceedings of the 19th ACM international confere

小tip-圖片載入狀態效果的實現

做的一個需求,其中有一個是實現類似於下圖的一個圖片上傳效果: 從本地上傳圖片到伺服器,然後伺服器響應返回這個圖片在伺服器上的連結地址,將這個連結地址所對應的圖片顯示到螢幕上,並且在此圖片資源完全下載下來之前,呈現一個動態 loading的展點陣圖,直到圖片完全下載後進行替換 小tip

高效的序列化方式——MessagePack

最近在弄一些資料分析方面的內容,發現很多時候資料瓶頸在模組之間的資料序列化和反序列化上了,原來專案中用的是Json,找了一圈發現Json.net在Json序列化庫中已經是效能的佼佼者了,便準備從序列化方式入手了,最後選擇了MessagePack的這個序列化的庫。 MessagePack是一種的序列化格式。這

C++棧的實現

#include <atomic> #include <string> #include <iostream> template<typename T> class tg_stack { public:     tg_stac

思考(四十五):通用郵件服務SDK的實現方法

SDK 製作思路 SDK 不干涉使用方使用什麼網路模組、協議 SDK 不干涉使用方伺服器組內部架構 使用方只需要關注 SDK 介面用法,不需要關注 SDK 內部郵件協議、格式 Client SDK

高效的android雙擊退出(可擴充套件多擊)

參考Google,安卓手機中在檢視安卓系統版本的地方,三擊或者多擊會出現彩蛋,可以借鑑其原始碼進行實現。 //利用陣列來儲存時間     long[] mHits = new long[3];     @Override     pub

機器視覺課內實驗:攝像機標定演算法的程式設計實現

機器視覺課內實驗:一種攝像機標定演算法的程式設計實現 一實驗目的 掌握攝像機標定方法的原理,採用一種攝像機標定演算法,程式設計實現攝像機內部引數和外部引數的估計。 二.標定原理 攝像機標定是指建立攝像機影象畫素位置與場景點位置之間的關係,其途徑是根據攝像機

基於vue的圖片載入狀態效果的實現

做的一個需求,其中有一個是實現類似於下圖的一個圖片上傳效果: 從本地上傳圖片到伺服器,然後伺服器響應返回這個圖片在伺服器上的連結地址,將這個連結地址所對應的圖片顯示到螢幕上,並且在此圖片資源完全下載下來之前,呈現一個動態 loading的展點陣圖,直到圖片完全

環形佇列框架Disruptor不同策略說明

* <pre> * BlockingWaitStrategy: 這是預設的策略,使用BlockingWaitStrategy和使用BlockingQueue是非常類似的, * 他們都使用鎖和條件Condition進行資料的監控和執行緒的喚醒,因為涉及到執行緒的切換,BlockingW

Liv Wild:高效的專案啟動方式——QuickStart

一種高效的專案啟動方式——QuickStart,該方法用以確認在專案開始之前理解專案的關鍵驅動因素。來自ThoughtWorks英國分公司的Liv Wild為現場觀眾介紹QuickStart的理念與實踐。 主持人:有請Liv Wild做主題演講,演講的題目是“一種高效的專案啟