歡迎光臨
每天分享高質量文章

當多執行緒併發遇到Actor

來自:碼農翻身(微信號:coderising)

1
多執行緒併發的難題

張大胖在做一個銀行相關的專案,寫了一個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框架。 ”

(完)


●本文編號598,以後想閱讀這篇文章直接輸入598即可

●輸入m獲取文章目錄

推薦↓↓↓

 

Web開發

更多推薦18個技術類微信公眾號

涵蓋:程式人生、演算法與資料結構、黑客技術與網絡安全、大資料技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。

赞(0)

分享創造快樂