從java的多線程到erlang的actor併發模型

多線程併發的難題

張大胖在做一個銀行相關的項目,寫了一個Account的類,用來表示一個用戶的銀行賬號,根據銀行的常規業務,自然要提供兩個方法,存款(deposit)和取款(withdraw)。

爲了防止多線程併發時導致的數據不一致問題,張大胖給每個方法都加了synchronized, 那意思很清楚,想進入某個方法執行存款或取款操作,必須得先獲得一把鎖纔行。

 

(注:爲了簡化,這裏沒有做邊界條件檢查。)

但是在做轉賬操作的時候,爲了保證一致性,必須得把兩個賬戶都加上鎖,然後纔可以操作,於是張大胖寫下了這樣的代碼,他覺得很簡單,立刻就提交給Bill ,讓他Review。

富有經驗的Bill立刻就發現了問題,馬上對張大胖說:“這樣會出現死鎖!”

張大胖說:“這麼簡單的代碼,怎麼可能有死鎖?”

“假設線程1 做的操作是賬戶A給賬戶B轉賬, 先鎖住了A賬戶, 接下來試圖申請B賬戶的鎖;

與此同時線程2 在從 賬戶B給賬戶A 轉賬, 先鎖住了B賬戶的鎖, 接下來試圖申請A賬戶的鎖。

兩個線程各自持有資源, 然後等待獲取對方的資源, 都無法執行下去, 死鎖就出現了!”

張大胖無言以對,不得不承認Bill是正確的。他問道:“那怎麼解決這個問題?”

“非常簡單,加鎖的時候按次序來就可以了,例如所有的線程,無論是從A向B轉賬,還是從B向A轉賬,都先獲得賬號A的鎖,成功後再獲得賬戶B的鎖,這樣就沒問題了。”

張大胖說:“那樣代碼會變得很古怪啊,還得給兩個賬戶排個順序,如果不知道背後的思想讀起來很痛苦,怪不得人家說多線程編程很難啊。”

Bill說:“是啊, 其實線程這個東西,就是一段代碼的執行而已, 是操作系統層面的概念,可是我們苦逼的程序員不得不來面對它,來背這個多線程併發的鍋了。”

2

黑盒子

下班後,張大胖一直在思考這個問題:既然線程是操作系統層面的概念,能不能把線程的概念隱藏起來,然後所有的操作都不用加鎖呢? 這樣以來編程就會容易得多啊!

本質的問題是什麼?

首先是共享的狀態,例如Account中的balance ,多個線程都要讀寫, 其次就是多個線程亂序、併發執行。

能不能換個思路,把這個Account對象看成一個黑盒子,你想存款了,就發一個存款的消息過來,想取款就發一個取款的消息過來。

不管是有一個消息,還是有100個消息,我統統放到黑盒子的一個隊例中,然後讓Account對象一個個順序處理不就可以了? 根本不用在方法上加鎖!

這樣做,其實就是把併發的操作變成了串行的操作而已!

不對,如果調用方把取款消息放下就走, 不等待返回結果, 那就不是同步操作,而是異步操作了!

但是如果取款的時候發現餘額不足,怎麼通知調用方?嗯,調用方也必須是個黑盒子對象,也向它發送異步消息,這個消息也會在消息隊列中存下來,調用方“黑盒子”也會一個個處理。

想到這一層,張大胖激動起來:取款和存款的操作就不用在加鎖了,碼農們只要考慮黑盒子對消息的處理即可:取出消息,處理消息,向別的黑盒子發送消息, 根本不用考慮線程這樣底層的概念了。

3

Actor模型

第二天張大胖趕緊找到Bill, 向他炫耀自己的“新發明”。

Bill不動聲色:“小夥子,不錯啊,重新發明了輪子!”

“重新發明?”

“是啊,你這個所謂黑盒子,就是所謂Actor模型啊! 它最早由Carl Hewitt在1973定義,其消息傳遞的方式更加符合面向對象的原始意圖, 這一點我想你也體會到了,要不你怎麼把他們叫做黑盒子啊。”

“1973年? 我還沒出生。唉,看來這些概念已經被老前輩們都發明完了啊。”

“Actor屬於併發組件模型 ,可以把程序員從多線程併發或線程池等基礎概念中解放出來。它有這麼幾個特點:”

Actor:

就是你說的黑盒子,系統是由很多Actor組成。 Actor之間不共享狀態,但是會接收別的Actor發送的異步消息,處理的過程中,會改變內部狀態,也可能向別的Actor發送消息。

Message:

消息是不可變的, 它的發送都是異步的,Actor內部有個“MailBox”來緩存消息。

MailBox:

Actor內部緩存消息的郵箱, 其他Actor發送的消息都放到這裏,然後被本Actor處理,類似有多個生成者和一個消費者的隊例。

張大胖說:“和我之前的圖差不多,看來我確實是重新發明了輪子啊。”

4

用Actor實現轉賬

Bill 笑道:“這個Actor看起來很美,但是編程的時候你得刷新一下你的思維纔行。 大胖,之前你的轉賬操作在多線程下不是會出現死鎖嗎? 你考慮下,如果用Actor的思路該怎麼寫?”

“首先,得有兩個Actor, 這兩個Actor 表示了兩個賬戶,我把它們叫做旺財和小強。”

“然後呢,轉賬的邏輯怎麼處理?”

張大胖想了一會:“既然轉賬是在兩個Actor之間發生的,那可以引入一個協調者Actor,叫做轉賬管家吧。不過,由於消息都是異步的,轉賬管家向旺財這個Actor發起扣款請求以後,不知道什麼時候才能真正執行扣款,也不能立刻知道是否成功,必須得等待啊,這就有點麻煩了。”

Bill說:“我給你畫個流程圖,你看看。”

張大胖感慨地說:“原來的多線程併發模型,需要同時鎖住兩個賬戶,然後才能進行轉賬。現在每個Actor都獨立,也把這個轉賬給搞定了。”

Bill說:“其實對於轉賬管家來說,對每個轉賬的消息,內部是隱含一個流程狀態的,就是先向某個賬戶扣款,成功以後再向另一個賬戶增加,最後給調用者返回狀態,這個次序是不能亂的。看到圖中那個Transaction ID沒有(Tx01),就是用來跟蹤這個轉賬的事務。”

4

漏洞

“我發現了一個漏洞,你這個轉賬雖然看起來很美,沒有加鎖,但是和原來的是有區別的,原來多線程思路是會把旺財和小強的賬戶同時鎖住,然後轉賬,在這個過程中,別人是不能操作這兩個賬號的! 而你的Actor方案中,當轉賬管家給旺財發消息扣款的時候,小強其實是自由的,如果這時候小強的賬戶被凍結,那你的轉賬管家還得回滾旺財的扣款,這多麻煩啊。”

Bill:“哈哈,你小子還挺機靈的嘛,看出了這個問題,Actor模型非常適用於多個組件獨立工作,相互之間僅僅依靠消息傳遞的情況。如果想在多個組件之間維持一致的狀態(比如咱們例子中的轉賬),那就不爽了。”

“那怎麼解決這個問題?”

“那必須得用一些特殊手段了,有些實現Actor的框架,例如Akka,專門提供了像Coordinated /Transactor這樣的機制來處理這個問題。有空的話給你仔細講講。”

“好吧,我回頭看看這個Akka, 對了, Actor雖然對用戶隱藏了線程, 但是總得有線程來處理消息吧。” 張大胖問道。

“那是肯定的,線程本質上就是一段代碼的執行,每個Actor在處理消息的時候,肯定得和線程關聯纔行,只不過Actor系統把線程這個概念給隱藏了。”

“有哪些系統實現了Actor?” 張大胖接着問。

“其實最著名的就是Erlang了,Actor模型可以說是它的基礎,除了我們上面所說的,還可以讓Actor之間建立關聯,例如讓一個Actor去監控另外一些Actor工作,如果那些Actor崩潰了,就新建一個Actor繼續工作。在Java 領域,剛纔提到的Akka是比較知名的一個Actor框架。 ”

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章