原文地址:Concurrency with LMAX Disruptor – An Introductionhtml
前些天在併發編程網,看到了關於 Disruptor 的介紹。感受此框架驚爲天人,值得學習學習。在把併發編程網上面介紹逐一瀏覽以後發覺,缺乏了對於 Disruptor 基礎應用的介紹。因而就有了翻譯海外基礎介紹的想法。git
要爲之後難以在工做中用到 Disruptor 而感到沮喪。由於據介紹來看,它號稱"可以在一個線程裏每秒處理6百萬訂單" 。我所在的平臺撐不起這個量,同時也限於學歷跟從業背景難以去這類大公司供職。github
追逐性能,經常來講你給老闆省了多少硬件,老闆是看不到的。
建議一開始仍是不要設計得性能太過優秀,否則老闆看不到你的價值。編程
Disruptor 是一個在併發編程中避免資源競爭的容器,用於協調生產者與消費者之間的關係,同時有着領域驅動模型 CQRS框架那種基於命令的影子。
應用這個框架編寫代碼將會較爲繁複,模塊與模塊以前的通訊全由一個又一個Event類來協調。
相對於大多數喜歡一個方法到底的開發同窗來講會比較麻煩,畢竟須要定義更多類。數組
本篇文章目的在於介紹 LMAX Disruptor,探討它是如何幫助咱們實現軟件低延遲、高併發特性。
咱們還將介紹 Disruptor 庫的基本用法。緩存
Disruptor 是由 LMAX 編寫的開源Java
庫。它是個併發編程框架,用於處理大量事務,並且低延遲(然而並不會像常規併發代碼那樣複雜)。
如此高效的性能優化,是經過更高效的利用底層硬件的設計實現。性能優化
讓咱們從機械情懷的核心概念開始 - 這就是了解底層硬件如何以最屌的方式運行。數據結構
舉個栗子,架構
到CPU的延遲 | CPU時鐘 | 耗時 |
---|---|---|
主內存 | 不少(Multiple) | ~60-80 ns |
L3 緩存 | ~40-45 週期 | ~15 ns |
L2 緩存 | ~10 週期 | ~3 ns |
L1 緩存 | ~3-4 週期 | ~1 ns |
寄存器 | 1 週期 | ~15 ns |
生產者和消費者之間經常速率不一致,隊列一般老是爲"空"或"滿"。所以隊列頭(head)、隊列尾(tail)和隊列大小(size)有着資源競爭(write contention)。生產和消費不多達到和諧的狀態。併發
一般採用鎖來解決資源競爭(write contention)問題,但與此同時又會陷入內核級別的上下文切換。當這種狀況發生時,處理器所緩存的數據可能丟失。(譯者注:當線程A、B分別在CPU上不一樣的兩個內核上運行時,線程A正要更新變量Y。不幸的是,這個變量也同時正要被線程B所更新。若是核心A得到了全部權,緩存子系統將會使核心B中對應的緩存行失效。當核心B得到了全部權而後執行更新操做,核心A就要使本身對應的緩存行失效。這會來來回回的通過L3緩存,大大影響了性能。)
爲了達到更好的線程可伸縮性,就必須確保不會有兩個寫線程操做同一個變量(多個讀線程是沒有問題的,如同處理器間調用高速連接獲取緩存)。隊列,它敗在了獨立寫入原則(one-writer principle)。
若是兩個不一樣的線程寫入隊列中兩個不一樣的值,那麼每一個內核都會使另一個線程的緩存行失效(數據在主內存與高速緩存之間的傳輸是作的固定大小的塊傳輸,稱之爲緩存行。譯者注:僞共享和緩存行)。儘管兩個線程寫入兩個不一樣的變量,也一樣會引發它們間的資源競爭。這叫作僞共享,由於每次訪問隊列頭(head),隊列尾(tail)也一樣會被加載到緩存行,反之亦然。
Disruptor 有一個基於數組的循環數據結構(環裝緩衝區)。這個循環數據結構,它是個擁有下個可用元素引用的數組。預先分配了對象內存空間。生產者與消費者經過這個循環數據結構進行讀寫操做,並不會有鎖或資源競爭。
在Disruptor 中,全部事件(events)以組播的方式被髮布給全部消費者,以便下游隊列經過並行的方式進行消費。由於消費者的並行消費,須要協調消費者間的依賴關係(依賴關係圖)。
生產者和消費者中有個序列計數器,指示緩衝區中當前正在被它所處理的元素。全部生產者或消費者都只能夠修改它本身的序列計數器,但同時能夠讀取其餘的序列計數器。
讓咱們把Disruptor 庫的依賴關係添加到 pom.xml中。
<dependency> <groupId>com.lmax</groupId> <artifactId>disruptor</artifactId> <version>3.3.6</version> </dependency>
最新版本的依賴關係能夠在這裏找到。
讓咱們來定義一個攜帶數據的 Event:
public static class ValueEvent { private int value; public final static EventFactory EVENT_FACTORY = () -> new ValueEvent(); // standard getters and setters }
這個 EventFactory 會讓 Disruptor分配事件。
消費者從環裝緩衝區讀取數據。讓咱們來定義個處理事件的消費者:
public class SingleEventPrintConsumer { ... public EventHandler<ValueEvent>[] getEventHandler() { EventHandler<ValueEvent> eventHandler = (event, sequence, endOfBatch) -> print(event.getValue(), sequence); return new EventHandler[] { eventHandler }; } private void print(int id, long sequenceId) { logger.info("Id is " + id + " sequence id that was used is " + sequenceId); } }
在咱們的示例中,消費者只是打印打印日誌。
構造 Disruptor:
ThreadFactory threadFactory = DaemonThreadFactory.INSTANCE; WaitStrategy waitStrategy = new BusySpinWaitStrategy(); Disruptor<ValueEvent> disruptor = new Disruptor<>( ValueEvent.EVENT_FACTORY, 16, threadFactory, ProducerType.SINGLE, waitStrategy);
在這個 Disruptor 的構造方法中,依次定義瞭如下參數:
鏈接消費者處理程序:
disruptor.handleEventsWith(getEventHandler());
Disruptor能夠提供多個消費者來處理生產者生成的數據。在上面的例子中,咱們只使用了一個消費者處理事件。
RingBuffer<ValueEvent> ringBuffer = disruptor.start();
生產者將參數按順序放置到環形緩衝區中。(譯者注:3.4所述Event Factory已經做爲參數,構造Disruptor對象)生產者必須獲取到到下個可用元素,以免覆蓋還沒有消耗的元素。
利用 RingBuffer 發佈事件:
for (int eventCount = 0; eventCount < 32; eventCount++) { long sequenceId = ringBuffer.next(); ValueEvent valueEvent = ringBuffer.get(sequenceId); valueEvent.setValue(eventCount); ringBuffer.publish(sequenceId); }
在此,生產者依次生產、發佈事件。值得注意的是 Disruptor 與2階段提交協議相似。它先獲取一個新序列號(sequenceId),再經過(sequenceId)獲取事件,而後製做事件,最後發佈。下次得到sequenceId + 1。
在本教程中,咱們已經闡述了 Disruptor是什麼,它是如何實現低延遲的併發處理。回顧了機械情懷的理念,以及如何利用它實現低延遲。最後展現了一個使用 Disruptor 庫的例子。
示例代碼能夠在GitHub項目中找到。這是一個基於Maven的項目,因此它很容易導入和運行。
DDD CQRS架構和傳統架構的優缺點比較
僞共享(False Sharing)
僞共享和緩存行
這次翻譯拖了快兩個月,糾結、消沉、迷離、迴歸。 開始以爲不斷的技術探索,彷彿只是對於前途的過多焦慮,讓本身更多的沉浸於忙碌,從而更多的擡頭看路。 看到不少人接下來的路,只是混混資歷跟業務。而後慢慢的加薪拿股權,就算是人工智能其實也沒有什麼明朗的技術變現路線。 技術再好,也須要自我營銷與宣傳。止步眼前,心中頗多不甘。