自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

剖析Disruptor:為什么會(huì)這么快?(一)鎖的缺點(diǎn)

開(kāi)發(fā) 后端
“Disruptor究竟是什么?"。目前我正準(zhǔn)備在回答這個(gè)問(wèn)題,但首先回答"為什么它會(huì)這么快?"

artin Fowler寫(xiě)了一篇非常好的文章,里面不僅提到了Disruptor,而且還解釋了Disruptor 如何應(yīng)用在LMAX的架構(gòu)里。里面有提及了一些目前沒(méi)有涉及的概念,但最經(jīng)常問(wèn)到的問(wèn)題是 “Disruptor究竟是什么?"。

目前我正準(zhǔn)備在回答這個(gè)問(wèn)題,但首先回答"為什么它會(huì)這么快?"

這些問(wèn)題持續(xù)出現(xiàn),但是我不能沒(méi)有說(shuō)它是干什么的就說(shuō)它為什么會(huì)這么快,不能沒(méi)有說(shuō)它為什么這樣做就說(shuō)它是什么東西。

所以我陷入了一個(gè)僵局,一個(gè)如何寫(xiě)博客的僵局。

要打破這個(gè)僵局,我準(zhǔn)備以最簡(jiǎn)單方式回答第一個(gè)問(wèn)題,如果幸運(yùn)的話,在以后博文里,如果需要解釋的話我會(huì)重新提回:Disruptor提供了一種線程之間信息交換的方式。

作為一個(gè)開(kāi)發(fā)者,因?yàn)椋⒕€程"一詞的出現(xiàn),我的警鐘已經(jīng)敲響,它意味著并發(fā),而并發(fā)是困難的。

并發(fā) 01

想象有兩個(gè)線程嘗試修改同一個(gè)變量value:

情況一:線程1先到達(dá)

  1. 變量value的值變?yōu)?rdquo;blah”。
  2. 然后當(dāng)線程2到達(dá)時(shí),變量value的值變?yōu)?rdquo;blahy”。

情況二:線程2先到達(dá)

  1. 變量value的值變?yōu)?rdquo;fluffy”。
  2. 然后當(dāng)線程1到達(dá)時(shí),值變?yōu)?rdquo;blah”。

情況三:線程1與線程2交互

  1. 線程2得到值"fluff"然后賦給本地變量myValue。
  2. 線程1改變value的值為”blah”。
  3. 然后線程2醒來(lái)并把變量value的值改為”fluffy”

情況三顯然是唯一一個(gè)是錯(cuò)誤的,除非除非你認(rèn)為wiki編輯的幼稚做法是正確的(Google Code Wiki,我一直在關(guān)注你)。其他兩種情況主要是看你的意圖和想要達(dá)到的效果。線程2可能不會(huì)關(guān)心變量value的值是什么,主要的意圖就是在后面加上字符 ‘y'而不管它原來(lái)的值是什么,在這種前提下,情況一和情況二都是正確的。

但是如果線程2只是想把"fluff"改為”fluffy”,那么情況二和三都不正確。假定線程2想把值設(shè)為”fluffy”,有幾種辦法可以解決這個(gè)問(wèn)題:

辦法一:悲觀鎖

(“No Entry”的標(biāo)志對(duì)于在沒(méi)有在英國(guó)開(kāi)車(chē)的人看得明白不?)

悲觀鎖和樂(lè)觀鎖這兩個(gè)詞通常在我們談?wù)摂?shù)據(jù)庫(kù)讀寫(xiě)時(shí)經(jīng)常會(huì)用到,但原理可以應(yīng)用到在獲得一個(gè)對(duì)象的鎖的情況。

只要線程2一獲得Entry 的互斥鎖,它就會(huì)阻擊其它線程去改變它,然后它就可以隨意做它要做的事情,設(shè)置值,然后做其它事情。

你可以想象這里非常耗性能的,因?yàn)槠渌€程在系統(tǒng)各處徘徊著準(zhǔn)備要獲得鎖然后又阻塞。線程越多,系統(tǒng)的響應(yīng)性就會(huì)越慢.

辦法二:樂(lè)觀鎖

在這種情況,當(dāng)線程2需要去寫(xiě)Entry時(shí)才會(huì)去鎖定它.它需要檢查Entry自從上次讀過(guò)后是否已經(jīng)被改過(guò)了。如果線程1在線程2讀完后到達(dá)并把值改為”blah”,線程2讀到了這個(gè)新值,線程2不會(huì)把"fluffy"寫(xiě)到Entry里并把線程1所寫(xiě)的數(shù)據(jù)覆蓋.線程2會(huì)重試(重新讀新的值,與舊值比較,如果相等則在變量的值后面附上’y’),這里在線程2不會(huì)關(guān)心新的值是什么的情況.或者線程2會(huì)拋出一個(gè)異常,或者會(huì)返回一個(gè)某些字段已更新的標(biāo)志,這是在期望把”fluff”改為”fluffy”的情況.舉一個(gè)第二種情況的例子,如果你和另外一個(gè)用戶同時(shí)更新一個(gè)Wiki的頁(yè)面,你會(huì)告訴另外一個(gè)用戶的線程 Thread 2,它們需要重新加載從Thread1來(lái)新的變化,然后再提交它們的內(nèi)容。

潛在的問(wèn)題:死鎖

鎖定會(huì)帶來(lái)各種各樣的問(wèn)題,比如死鎖,想象有2個(gè)線程需要訪問(wèn)兩個(gè)資源

如果你濫用鎖技術(shù),兩個(gè)鎖都在獲得鎖的情況下嘗試去獲得另外一個(gè)鎖,那就是你應(yīng)該重啟你的電腦的時(shí)候了。(校注:作者還挺幽默)

很明確的一個(gè)問(wèn)題:鎖技術(shù)是慢的..

關(guān)于鎖就是它們需要操作系統(tǒng)去做裁定。線程就像兩姐妹在為一個(gè)玩具在爭(zhēng)吵,然后操作系統(tǒng)就是能決定他們誰(shuí)能拿到玩具的父母,就像當(dāng)你跑向你父親告訴他你的姐姐在你玩著的時(shí)候搶走了你的變形金剛-他還有比你們爭(zhēng)吵更大的事情去擔(dān)心,他或許在解決你們爭(zhēng)吵之前要啟動(dòng)洗碗機(jī)并把它擺在洗衣房里。如果你把你的注意力放在鎖上,不僅要花時(shí)間來(lái)讓操作系統(tǒng)來(lái)裁定。Disruptor論文中講述了我們所做的一個(gè)實(shí)驗(yàn)。這個(gè)測(cè)試程序調(diào)用了一個(gè)函數(shù),該函數(shù)會(huì)對(duì)一個(gè) 64位的計(jì)數(shù)器循環(huán)自增5億次。當(dāng)單線程無(wú)鎖時(shí),程序耗時(shí)300ms。如果增加一個(gè)鎖(仍是單線程、沒(méi)有競(jìng)爭(zhēng)、僅僅增加鎖),程序需要耗時(shí) 10000ms,慢了兩個(gè)數(shù)量級(jí)。更令人吃驚的是,如果增加一個(gè)線程(簡(jiǎn)單從邏輯上想,應(yīng)該比單線程加鎖快一倍),耗時(shí)224000ms。使用兩個(gè)線程對(duì)計(jì)數(shù)器自增5億次比使用無(wú)鎖單線程慢1000倍。并發(fā)很難而鎖的性能糟糕。我僅僅是揭示了問(wèn)題的表面,而且,這個(gè)例子很簡(jiǎn)單。但重點(diǎn)是,如果代碼在多線程環(huán)境中執(zhí)行,作為開(kāi)發(fā)者將會(huì)遇到更多的困難:

  • 代碼沒(méi)有按設(shè)想的順序執(zhí)行。上面的場(chǎng)景3表明,如果沒(méi)有注意到多線程訪問(wèn)和寫(xiě)入相同的數(shù)據(jù),事情可能會(huì)很糟糕。
  • 減慢系統(tǒng)的速度。場(chǎng)景3中,使用鎖保護(hù)代碼可能導(dǎo)致諸如死鎖或者效率問(wèn)題。

這就是為什么許多公司在面試時(shí)會(huì)多少問(wèn)些并發(fā)問(wèn)題(當(dāng)然針對(duì)Java面試)。不幸的是,即使未能理解問(wèn)題的本質(zhì)或沒(méi)有問(wèn)題的解決方案,也很容易學(xué)會(huì)如何回答這些問(wèn)題。

#p#

Disruptor如何解決這些問(wèn)題。

首先,Disruptor根本就不用鎖。

取而代之的是,在需要確保操作是線程安全的(特別是,在多生產(chǎn)者的環(huán)境下,更新下一個(gè)可用的序列號(hào))地方,我們使用CAS(Compare And Swap/Set)操作。這是一個(gè)CPU級(jí)別的指令,在我的意識(shí)中,它的工作方式有點(diǎn)像樂(lè)觀鎖——CPU去更新一個(gè)值,但如果想改的值不再是原來(lái)的值,操作就失敗,因?yàn)楹苊黠@,有其它操作先改變了這個(gè)值。

注意,這可以是CPU的兩個(gè)不同的核心,但不會(huì)是兩個(gè)獨(dú)立的CPU。

CAS操作比鎖消耗資源少的多,因?yàn)樗鼈儾粻可娌僮飨到y(tǒng),它們直接在CPU上操作。但它們并非沒(méi)有代價(jià)——在上面的試驗(yàn)中,單線程無(wú)鎖耗時(shí) 300ms,單線程有鎖耗時(shí)10000ms,單線程使用CAS耗時(shí)5700ms。所以它比使用鎖耗時(shí)少,但比不需要考慮競(jìng)爭(zhēng)的單線程耗時(shí)多。

回到Disruptor,在我講生產(chǎn)者時(shí)講過(guò)ClaimStrategy。在這些代碼中,你可以看見(jiàn)兩個(gè)策略,一個(gè)是SingleThreadedStrategy(單線程策略)另一個(gè)是 MultiThreadedStrategy(多線程策略)。你可能會(huì)有疑問(wèn),為什么在只有單個(gè)生產(chǎn)者時(shí)不用多線程的那個(gè)策略?它是否能夠處理這種場(chǎng)景?當(dāng)然可以。但多線程的那個(gè)使用了AtomicLong(Java提供的CAS操作),而單線程的使用long,沒(méi)有鎖也沒(méi)有CAS。這意味著單線程版本會(huì)非???,因?yàn)樗挥幸粋€(gè)生產(chǎn)者,不會(huì)產(chǎn)生序號(hào)上的沖突。

我知道,你可能在想:把一個(gè)數(shù)字轉(zhuǎn)成AtomicLong不可能是Disruptor速度快的唯一秘密。當(dāng)然,它不是,否則它不可能稱為“為什么這么快(第一部分)”。

但這是非常重要的一點(diǎn)——在整個(gè)復(fù)雜的框架中,只有這一個(gè)地方出現(xiàn)多線程競(jìng)爭(zhēng)修改同一個(gè)變量值。這就是秘密。還記得所有的訪問(wèn)對(duì)象都擁有序號(hào)嗎?如果只有一個(gè)生產(chǎn)者,那么系統(tǒng)中的每一個(gè)序列號(hào)只會(huì)由一個(gè)線程寫(xiě)入。這意味著沒(méi)有競(jìng)爭(zhēng)、不需要鎖、甚至不需要CAS。在ClaimStrategy中,如果存在多個(gè)生產(chǎn)者,唯一會(huì)被多線程競(jìng)爭(zhēng)寫(xiě)入的序號(hào)就是 ClaimStrategy 對(duì)象里的那個(gè)。

這也是為什么Entry中的每一個(gè)變量都只能被一個(gè)消費(fèi)者寫(xiě)。它確保了沒(méi)有寫(xiě)競(jìng)爭(zhēng),因此不需要鎖或者CAS。

回到為什么隊(duì)列不能勝任這個(gè)工作

因此你可能會(huì)有疑問(wèn),為什么隊(duì)列底層用RingBuffer來(lái)實(shí)現(xiàn),仍然在性能上無(wú)法與 Disruptor 相比。隊(duì)列和最簡(jiǎn)單的ring buffer只有兩個(gè)指針——一個(gè)指向隊(duì)列的頭,一個(gè)指向隊(duì)尾:

如果有超過(guò)一個(gè)生產(chǎn)者想要往隊(duì)列里放東西,尾指針就將成為一個(gè)沖突點(diǎn),因?yàn)橛卸鄠€(gè)線程要更新它。如果有多個(gè)消費(fèi)者,那么頭指針就會(huì)產(chǎn)生競(jìng)爭(zhēng),因?yàn)樵乇幌M(fèi)之后,需要更新指針,所以不僅有讀操作還有寫(xiě)操作了。

等等,我聽(tīng)到你喊冤了!因?yàn)槲覀円呀?jīng)知道這些了,所以隊(duì)列常常是單生產(chǎn)者和單消費(fèi)者(或者至少在我們的測(cè)試?yán)锸牵?/p>

隊(duì)列的目的就是為生產(chǎn)者和消費(fèi)者提供一個(gè)地方存放要交互的數(shù)據(jù),幫助緩沖它們之間傳遞的消息。這意味著緩沖常常是滿的(生產(chǎn)者比消費(fèi)者快)或者空的(消費(fèi)者比生產(chǎn)者快)。生產(chǎn)者和消費(fèi)者能夠步調(diào)一致的情況非常少見(jiàn)。

所以,這就是事情的真面目。一個(gè)空的隊(duì)列:

一個(gè)滿的隊(duì)列:

(校對(duì)注:這應(yīng)該是一個(gè)雙向隊(duì)列)

隊(duì)列需要保存一個(gè)關(guān)于大小的變量,以便區(qū)分隊(duì)列是空還是滿。否則,它需要根據(jù)隊(duì)列中的元素的內(nèi)容來(lái)判斷,這樣的話,消費(fèi)一個(gè)節(jié)點(diǎn)(Entry)后需要做一次寫(xiě)入來(lái)清除標(biāo)記,或者標(biāo)記節(jié)點(diǎn)已經(jīng)被消費(fèi)過(guò)了。無(wú)論采用何種方式實(shí)現(xiàn),在頭、尾和大小變量上總是會(huì)有很多競(jìng)爭(zhēng),或者如果消費(fèi)操作移除元素時(shí)需要使用一個(gè)寫(xiě)操作,那元素本身也包含競(jìng)爭(zhēng)。

基于以上,這三個(gè)變量常常在一個(gè)cache line里面,有可能導(dǎo)致false sharing。因此,不僅要擔(dān)心生產(chǎn)者和消費(fèi)者同時(shí)寫(xiě)size變量(或者元素),還要注意由于頭指針尾指針在同一位置,當(dāng)頭指針更新時(shí),更新尾指針會(huì)導(dǎo)致緩存不命中。這篇文章已經(jīng)很長(zhǎng)了,所以我就不再詳述細(xì)節(jié)了。

這就是我們所說(shuō)的“分離競(jìng)爭(zhēng)點(diǎn)問(wèn)題”或者隊(duì)列的“合并競(jìng)爭(zhēng)點(diǎn)問(wèn)題”。通過(guò)將所有的東西都賦予私有的序列號(hào),并且只允許一個(gè)消費(fèi)者寫(xiě)Entry對(duì)象中的變量來(lái)消除競(jìng)爭(zhēng),Disruptor 唯一需要處理訪問(wèn)沖突的地方,是多個(gè)生產(chǎn)者寫(xiě)入 Ring Buffer 的場(chǎng)景。

總結(jié)

Disruptor相對(duì)于傳統(tǒng)方式的優(yōu)點(diǎn):

  1. 沒(méi)有競(jìng)爭(zhēng)=沒(méi)有鎖=非???。
  2. 所有訪問(wèn)者都記錄自己的序號(hào)的實(shí)現(xiàn)方式,允許多個(gè)生產(chǎn)者與多個(gè)消費(fèi)者共享相同的數(shù)據(jù)結(jié)構(gòu)。
  3. 在每個(gè)對(duì)象中都能跟蹤序列號(hào)(ring buffer,claim Strategy,生產(chǎn)者和消費(fèi)者),加上神奇的cache line padding,就意味著沒(méi)有為偽共享和非預(yù)期的競(jìng)爭(zhēng)。

校訂:需要注意Disruptor2.0使用了與本文中不一樣的名字。如果對(duì)類名感到困惑,請(qǐng)參考我的變更匯總。

原文鏈接:http://mechanitis.blogspot.com/2011/07/dissecting-disruptor-why-its-so-fast.html(有墻)

http://ifeve.com/disruptor-locks-are-bad/

譯文鏈接:http://ifeve.com/locks-are-bad/

 

責(zé)任編輯:陳四芳 來(lái)源: ifeve.com
相關(guān)推薦

2013-06-14 10:12:22

共享并行

2013-06-19 10:55:40

Disruptor并發(fā)框架

2013-06-18 10:30:45

Disruptor框架

2020-03-30 15:05:46

Kafka消息數(shù)據(jù)

2020-02-27 21:03:30

調(diào)度器架構(gòu)效率

2020-02-27 15:44:41

Nginx服務(wù)器反向代理

2024-02-26 21:15:20

Kafka緩存參數(shù)

2024-11-26 08:52:34

SQL優(yōu)化Kafka

2022-01-04 08:54:32

Redis數(shù)據(jù)庫(kù)數(shù)據(jù)類型

2023-08-29 07:46:08

Redis數(shù)據(jù)ReHash

2021-05-27 20:56:51

esbuild 工具JavaScript

2020-10-15 09:19:36

Elasticsear查詢速度

2012-08-17 10:01:07

云計(jì)算

2023-03-21 08:02:36

Redis6.0IO多線程

2019-02-18 08:10:53

2020-10-21 09:17:52

Redis面試內(nèi)存

2020-04-27 07:13:37

Nginx底層進(jìn)程

2021-03-18 14:34:34

達(dá)達(dá)集團(tuán)京東云電商

2023-11-02 10:22:29

gRPC后端通信

2024-07-24 08:38:07

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)