鍍金池/ 教程/ Java/ 揭秘內(nèi)存屏障
Ringbuffer的特別之處
寫入 Ringbuffer
鎖的缺點(diǎn)
神奇的緩存行填充
如何從Ringbuffer讀取
Disruptor Wizard已死,Disruptor Wizard永存!
線程間共享數(shù)據(jù)無需競(jìng)爭(zhēng)
LMAX Disruptor——一個(gè)高性能、低延遲且簡(jiǎn)單的框架
通過 Axon 和 Disruptor處理 1M tps
揭秘內(nèi)存屏障
Disruptor (無鎖并發(fā)框架)-發(fā)布
LMAX架構(gòu)
偽共享(False Sharing)
解析 Disruptor 的依賴關(guān)系
Disruptor 2.0 更新摘要

揭秘內(nèi)存屏障

原文地址:http://ifeve.com/disruptor-memory-barriers/

譯者:杜建雄 校對(duì):歐振聰

最近我博客文章更新有點(diǎn)慢,因?yàn)槲以诿χ鴮懸黄榻B內(nèi)存屏障(Memory Barries)以及如何將其應(yīng)用于 Disruptor 的文章。問題是,無論我翻閱了多少資料,向耐心的 MartinMike 請(qǐng)教了多少遍,以試圖理清一些知識(shí)點(diǎn),可我總是不能直觀地抓到重點(diǎn)。大概是因?yàn)槲也痪邆渖詈竦谋尘爸R(shí)來幫助我透徹理解。

所以,與其像個(gè)傻瓜一樣試圖去解釋一些自己都沒完全弄懂的東西,還不如在抽象和大量簡(jiǎn)化的層次上,把我在該領(lǐng)域所掌握的知識(shí)分享給大家 。Martin 已經(jīng)寫了一篇文章《going into memory barriers》介紹內(nèi)存屏障的一些具體細(xì)節(jié),所以我就略過不說了。

免責(zé)聲明:文章中如有錯(cuò)誤全由本人負(fù)責(zé),與 Disruptor 的實(shí)現(xiàn)和 LMAX 里真正懂這些知識(shí)的大牛們無關(guān)。

主題是什么?

我寫這個(gè)系列的博客主要目的是解析 Disruptor 是如何工作的,并深入了解下為什么這樣工作。理論上,我應(yīng)該從可能準(zhǔn)備使用 disruptor 的開發(fā)人員的角度來寫,以便在代碼和技術(shù)論文[Disruptor-1.0.pdf]之間搭建一座橋梁。這篇文章提及到了內(nèi)存屏障,我想弄清楚它們到底是什么,以及它們是如何應(yīng)用于實(shí)踐中的。

什么是內(nèi)存屏障?

它是一個(gè) CPU 指令。沒錯(cuò),又一次,我們?cè)谟懻?CPU 級(jí)別的東西,以便獲得我們想要的性能(Martin 著名的 Mechanical Sympathy理論)?;旧?,它是這樣一條指令:

a)確保一些特定操作執(zhí)行的順序; b)影響一些數(shù)據(jù)的可見性(可能是某些指令執(zhí)行后的結(jié)果)。

編譯器和 CPU 可以在保證輸出結(jié)果一樣的情況下對(duì)指令重排序,使性能得到優(yōu)化。插入一個(gè)內(nèi)存屏障,相當(dāng)于告訴 CPU 和編譯器先于這個(gè)命令的必須先執(zhí)行,后于這個(gè)命令的必須后執(zhí)行。正如去拉斯維加斯旅途中各個(gè)站點(diǎn)的先后順序在你心中都一清二楚。

http://wiki.jikexueyuan.com/project/disruptor-getting-started/images/4-1.png" alt="" />

內(nèi)存屏障另一個(gè)作用是強(qiáng)制更新一次不同 CPU 的緩存。例如,一個(gè)寫屏障會(huì)把這個(gè)屏障前寫入的數(shù)據(jù)刷新到緩存,這樣任何試圖讀取該數(shù)據(jù)的線程將得到最新值,而不用考慮到底是被哪個(gè) cpu 核心或者哪顆 CPU 執(zhí)行的。

和Java有什么關(guān)系?

現(xiàn)在我知道你在想什么——這不是匯編程序。它是 Java。

這里有個(gè)神奇咒語叫 volatile (我覺得這個(gè)詞在 Java 規(guī)范中從未被解釋清楚)。如果你的字段是 volatile,Java 內(nèi)存模型將在寫操作后插入一個(gè)寫屏障指令,在讀操作前插入一個(gè)讀屏障指令。

http://wiki.jikexueyuan.com/project/disruptor-getting-started/images/4-2.png" alt="" />

這意味著如果你對(duì)一個(gè) volatile 字段進(jìn)行寫操作,你必須知道:

1、一旦你完成寫入,任何訪問這個(gè)字段的線程將會(huì)得到最新的值。

2、在你寫入前,會(huì)保證所有之前發(fā)生的事已經(jīng)發(fā)生,并且任何更新過的數(shù)據(jù)值也是可見的,因?yàn)閮?nèi)存屏障會(huì)把之前的寫入值都刷新到緩存。

舉個(gè)例子唄!

很高興你這樣說了。又是時(shí)候讓我來畫幾個(gè)甜甜圈了。

RingBuffe r的指針(cursor)(譯注:指向隊(duì)尾元素)屬于一個(gè)神奇的 volatile 變量,同時(shí)也是我們能夠不用鎖操作就能實(shí)現(xiàn) Disruptor 的原因之一。

http://wiki.jikexueyuan.com/project/disruptor-getting-started/images/4-3.png" alt="" />

生產(chǎn)者將會(huì)取得下一個(gè) Entry(或者是一批),并可對(duì)它(們)作任意改動(dòng), 把它(們)更新為任何想要的值。如你所知,在所有改動(dòng)都完成后,生產(chǎn)者對(duì) ring buffer 調(diào)用 commit 方法來更新序列號(hào)(譯注:把 cursor 更新為該 Entry 的序列號(hào))。對(duì) volatile 字段(cursor)的寫操作創(chuàng)建了一個(gè)內(nèi)存屏障,這個(gè)屏障將刷新所有緩存里的值(或者至少相應(yīng)地使得緩存失效)。

這時(shí)候,消費(fèi)者們能獲得最新的序列號(hào)碼(8),并且因?yàn)閮?nèi)存屏障保證了它之前執(zhí)行的指令的順序,消費(fèi)者們可以確信生產(chǎn)者對(duì) 7 號(hào) Entry 所作的改動(dòng)已經(jīng)可用。

…那么消費(fèi)者那邊會(huì)發(fā)生什么?

消費(fèi)者中的序列號(hào)是 volatile 類型的,會(huì)被若干個(gè)外部對(duì)象讀取——其他的下游消費(fèi)者可能在跟蹤這個(gè)消費(fèi)者。ProducerBarrier/RingBuffer(取決于你看的是舊的還是新的代碼)跟蹤它以確保環(huán)沒有出現(xiàn)重疊(wrap)的情況(譯注:為了防止下游的消費(fèi)者和上游的消費(fèi)者對(duì)同一個(gè) Entry 競(jìng)爭(zhēng)消費(fèi),導(dǎo)致在環(huán)形隊(duì)列中互相覆蓋數(shù)據(jù),下游消費(fèi)者要對(duì)上游消費(fèi)者的消費(fèi)情況進(jìn)行跟蹤)。

http://wiki.jikexueyuan.com/project/disruptor-getting-started/images/4-4.png" alt="" />

所以,如果你的下游消費(fèi)者(C2)看見前一個(gè)消費(fèi)者(C1)在消費(fèi)號(hào)碼為 12 的 Entry,當(dāng) C2 的讀取也到了 12,它在更新序列號(hào)前將可以獲得 C1 對(duì)該 Entry 的所作的更新。

基本來說就是,C1 更新序列號(hào)前對(duì) ring buffer 的所有操作(如上圖黑色所示),必須先發(fā)生,待 C2 拿到 C1 更新過的序列號(hào)之后,C2 才可以為所欲為(如上圖藍(lán)色所示)。

對(duì)性能的影響

內(nèi)存屏障作為另一個(gè) CPU 級(jí)的指令,沒有鎖那樣大的開銷。內(nèi)核并沒有在多個(gè)線程間干涉和調(diào)度。但凡事都是有代價(jià)的。內(nèi)存屏障的確是有開銷的——編譯器/cpu不能重排序指令,導(dǎo)致不可以盡可能地高效利用 CPU,另外刷新緩存亦會(huì)有開銷。所以不要以為用 volatile 代替鎖操作就一點(diǎn)事都沒。

你會(huì)注意到 Disruptor 的實(shí)現(xiàn)對(duì)序列號(hào)的讀寫頻率盡量降到最低。對(duì) volatile 字段的每次讀或?qū)懚际窍鄬?duì)高成本的操作。但是,也應(yīng)該認(rèn)識(shí)到在批量的情況下可以獲得很好的表現(xiàn)。如果你知道不應(yīng)對(duì)序列號(hào)頻繁讀寫,那么很合理的想到,先獲得一整批 Entries ,并在更新序列號(hào)前處理它們。這個(gè)技巧對(duì)生產(chǎn)者和消費(fèi)者都適用。以下的例子來自BatchConsumer:

    long nextSequence = sequence + 1;
    while (running)
    {
        try
        {
            final long availableSequence = consumerBarrier.waitFor(nextSequence);
            while (nextSequence <= availableSequence)
            {
                entry = consumerBarrier.getEntry(nextSequence);
                handler.onAvailable(entry);
                nextSequence++;
            }
            handler.onEndOfBatch();
            sequence = entry.getSequence();
        }
        …
        catch (final Exception ex)
        {
            exceptionHandler.handle(ex, entry);
            sequence = entry.getSequence();
            nextSequence = entry.getSequence() + 1;
        }
    }

(你會(huì)注意到,這是個(gè)舊式的代碼和命名習(xí)慣,因?yàn)檫@是摘自我以前的博客文章,我認(rèn)為如果直接轉(zhuǎn)換為新式的代碼和命名習(xí)慣會(huì)讓人有點(diǎn)混亂)

在上面的代碼中,我們?cè)谙M(fèi)者處理 entries 的循環(huán)中用一個(gè)局部變量(nextSequence)來遞增。這表明我們想盡可能地減少對(duì) volatile 類型的序列號(hào)的進(jìn)行讀寫。

總結(jié)

內(nèi)存屏障是 CPU 指令,它允許你對(duì)數(shù)據(jù)什么時(shí)候?qū)ζ渌M(jìn)程可見作出假設(shè)。在 Java 里,你使用 volatile 關(guān)鍵字來實(shí)現(xiàn)內(nèi)存屏障。使用 volatile 意味著你不用被迫選擇加鎖,并且還能讓你獲得性能的提升。

但是,你需要對(duì)你的設(shè)計(jì)進(jìn)行一些更細(xì)致的思考,特別是你對(duì) volatile 字段的使用有多頻繁,以及對(duì)它們的讀寫有多頻繁。

PS:上文中講到的 Disruptor中 使用的 New World Order 是一種完全不同于我目前為止所發(fā)表的博文中的命名習(xí)慣。我想下一篇文章會(huì)對(duì)舊式的和新式的命名習(xí)慣做一個(gè)對(duì)照。

延伸閱讀:

[1] 一種高效無鎖內(nèi)存隊(duì)列的實(shí)現(xiàn)

[2] Lock-free publishing

[3] Disruptor 系列譯文

原創(chuàng)文章,轉(zhuǎn)載請(qǐng)注明: 轉(zhuǎn)載自并發(fā)編程網(wǎng) – ifeve.com