Actor 併發模型 & "不要通過共享內存來通信,而應該通過通信來共享內存"

Actor 併發模型 & "不要通過共享內存來通信,而應該通過通信來共享內存"

Actors編程模型

  Actors模型首先是由Carl Hewitt在1973定義, 由Erlang OTP (Open Telecom Platform) 推廣,其

消息傳遞更加符合面向對象的原始意圖。

  • 隔離計算實體
  • "Share nothing"
  • 沒有任何地方同步
  • 異步消息傳遞
  • 不可變的消息 (消息模型類似mailbox / 消息隊列)

   傳統多數流行的語言併發是基於多線程之間的共享內存,使用同步方法防止寫爭奪,Actors使用消息模型,每個Actor在同一時間處理最多一個消息,可以發送消息給其他Actors,保證了單獨寫原則 。從而巧妙避免了多線程寫爭奪。

PS:此外,可以去 http://s3.amazonaws.com/four.livejournal/20091117/jsconf.pdf 看下 Node.js 是爲了解決什麼問題而誕生的,然後,再看下許式偉的《Go語言編程》一書中第4章(併發編程)以更好地理解"不要通過共享內存來通信,而應該通過通信來共享內存"。


問題:

看了壇裏幾篇actor的幾篇文章,可是不能很好的理解,需要大家共同指導討論下

1.actor併發模型的應用場景?
2.actor的原理?思維方式改變?
3.actor最簡單的demo?

解答:

1. actor併發模型的應用場景?
適合有狀態或者稱可變狀態的業務場景,如果用DDD術語,適合聚合根,具體案例如訂單,訂單有狀態,比如未付款未發貨,已經付款未發貨,已付款已發貨,導致訂單狀態的變化是事件行爲,比如付款行爲導致訂單狀態切換到"已經付款未發貨"。
如果知曉GOF設計模式的狀態模式,就更好理解有態概念。

2. actor的原理?思維方式改變?
行爲導致狀態變化,行爲執行是依靠線程,比如用戶發出一個付款的請求,服務器後端派出一個線程來執行付款請求,攜帶付款的金額和銀行卡等等信息,當付款請求被成功完成後,線程還要做的事情就是改變訂單狀態,這時線程訪問訂單的一個方法比如changeState。
如果後臺有管理員同時修改這個訂單狀態,那麼實際有兩個線程共同訪問同一個數據,這時就必須鎖,比如我們在changeState方法前加上sychronized這樣同步語法。
使用同步語法壞處是每次只能一個線程進行處理,如同上廁所,只有一個蹲坑,人多就必須排隊,這種情況性能很低。
如何避免鎖?
避免changeState方法被外部兩個線程同時佔用訪問,那麼我們自己設計專門的線程守護訂單狀態,而不是普通方法代碼,普通方法代碼比較弱勢,容易被外部線程hold住,而我們設計的這個對象沒有普通方法,只有線程,這樣就變成Order的守護線程和外部訪問請求線程的通訊問題了。
Actor採取的這種類似消息機制的方式,實際在守護線程和外部線程之間有一個隊列,俗稱信箱,外部線程只要把請求放入,守護線程就讀取進行處理。
這種異步高效方式是Actor基本原理,以ERlang和Scala語言爲主要特徵,他們封裝得更好,類似將消息隊列微觀化了。
我個人認爲要使用好Actor,還是要樹立自己對有態和無態的敏感性,這是幾年前我宣傳EJB的有態和無態Bean強調的一點,如果沒有這個敏感性,按照DDD第一找出聚合根的分析方法也能抓住重點。
當然這些思維的前提是拋棄數據庫中心思維,不要老是想着把狀態存在數據庫中,然後用SQL不斷修改,這是很低效的,也是有鎖,比Java等語言的同步鎖性能更差
以我個人來說經歷的步驟如下:
1.用數據表一個字段來表示狀態,比如1表示已付款未發貨,2表示已付款已發貨,然後用戶來一個請求用SQL修改。
2.用ORM實現,比如Hibernate JPA來修改狀態,雖然不用SQL了,但是Hibernate的悲觀鎖和樂觀鎖也讓人抓狂。
3.徹底拋棄數據庫,直接在內存緩存中進行修改,使用Java的同步鎖,性能還是不夠,吞吐量上不去。
4.Actor模型。

問題:

如果這兩個線程在一個jvm裏還行,可是遇到分佈式的情況咋辦啊。。?尤其是spring提倡的那種分佈式

解答:

這是可伸縮的,Actor對象(聚合根)本身有類似IO進出,就像信箱有收件箱和發件箱一樣,Actor可以通過發件箱向一個MQ等消息總線發送消息就可實現分佈式。
發件箱的線程隊列實現可藉助OneToOneConcurrentArrayQueue,只有一個訂閱者和發佈者,發佈者是Actor自己,訂閱者是一個Socket端口或MQ消息總線的發佈者。http://www.jdon.com/45504
分佈式網絡是否可靠?http://www.jdon.com/45519
區分不變與可變,也就是區分無態和有態,也就是區分實體(可變)與值對象(不變),是統領一切的編程法門(無論是多線程還是分佈式)。


擴展閱讀:

Actor的原理:
先從著名的c10k問題談起。有一個叫Dan Kegel的人在網上(http://www.kegel.com/c10k.html)提出:現在的硬件應該能夠讓一臺機器支持10000個併發的client。然後他討論了用不同的方式實現大規模併發服務的技術,歸納起來就是兩種方式:一個client一個thread,用blocking I/O;多個clients一個thread,用nonblocking I/O或者asynchronous I/O。目前asynchronous I/O的支持在Linux上還不是很好,所以一般都是用nonblocking I/O。大多數的實現都是用epoll()的edge triggering(傳統的select()有很大的性能問題)。這就引出了thread和event之爭,因爲前者就是完全用線程來處理併發,後者是用事件驅動(注意:這裏的事件驅動不是領域驅動開發中的事件驅動,只是術語名稱相同而已)來處理併發。當然實際的系統當中往往是混合系統:用事件驅動來處理網絡時間,而用線程來處理事務。由於目前操作系統(尤其是Linux)和程序語言的限制(Java/C/C++等),線程無法實現大規模的併發事務。一般的機器,要保證性能的話,線程數量基本要限制幾百(Linux上的線程有個特點,就是達到一定數量以後,會導致系統性能指數下降,參看SEDA的論文)。所以現在很多高性能web server都是使用事件驅動機制,比如nginx,Tornado,node.js等等。事件驅動幾乎成了高併發的同義詞,一時間紅的不得了。
其實線程和事件,或者說同步和異步之爭早就在學術領域爭了幾十年了。1978年有人爲了平息爭論,寫了論文證明了用線性的process(線程的模式)和消息傳遞(事件的模式)是等價的,而且如果實現合適,兩者應該有同等性能。當然這是理論上的。針對事件驅動的流行,2003年加大伯克利發表了一篇論文叫“Why events are a bad idea (for high-concurrency servers)”,指出其實事件驅動並沒有在功能上有比線程有什麼優越之處,但編程要麻煩很多,而且特別容易出錯。線程的問題,無非是目前的實現的原因。一個是線程佔的資源太大,一創建就分配幾個MB的stack,一般的機器能支持的線程大受限制。針對這點,可以用自動擴展的stack,創建的先少分點,然後動態增加。第二個是線程的切換負擔太大,Linux中實際上process和thread是一回事,區別就在於是否共享地址空間。解決這個問題的辦法是用輕量級的線程實現,通過合作式的辦法來實現共享系統的線程。這樣一個是切換的花費很少,另外一個可以維護比較小的stack。他們用coroutine和nonblocking I/O(用的是poll()+thread pool)實現了一個原型系統,證明了性能並不比事件驅動差。
那是不是說明線程只要實現的好就行了呢。也不完全對。2006年還是加大伯克利,發表了一篇論文叫“The problem with threads”。線程也不行。原因是這樣的。目前的程序的模型基本上是基於順序執行。順序執行是確定性的,容易保證正確性。而人的思維方式也往往是單線程的。線程的模式是強行在單線程,順序執行的基礎上加入了併發和不確定性。這樣程序的正確性就很難保證。線程之間的同步是通過共享內存來實現的,你很難來對併發線程和共享內存來建立數學模型,其中有很大的不確定性,而不確定性是編程的巨大敵人。作者以他們的一個項目中的經驗來說明,保證多線程的程序的正確性,幾乎是不可能的事情。首先,很多很簡單的模式,在多線程的情況下,要保證正確性,需要注意很多非常微妙的細節,否則就會導致deadlock或者race condition。其次,由於人的思維的限制,即使你採取各種消除不確定的辦法,比如monitor,transactional memory,還有promise/future,等等機制,還是很難保證面面俱到。以作者的項目爲例,他們有計算機科學的專家,有最聰明的研究生,採用了整套軟件工程的流程:design review, code review, regression tests, automated code coverage metrics,認爲已經消除了大多數問題,不過還是在系統運行4年以後,出現了一個deadlock。作者說,很多多線程的程序實際上存在併發錯誤,只不過由於硬件的並行度不夠,往往不顯示出來。隨着硬件的並行度越來越高,很多原來運行完好的程序,很可能會發生問題。我自己的體會也是,程序NullPointerException,core dump都不怕,最怕的就是race condition和deadlock,因爲這些都是不確定的(non-deterministic),往往很難重現。
那既然線程+共享內存不行,什麼樣的模型可以幫我們解決併發計算的問題呢。研究領域已經發展了一些模型,目前越來越多地開始被新的程序語言採用。最主要的一個就是Actor模型。它的主要思想就是用一些併發的實體,稱爲actor,他們之間通過發送消息來同步。所謂“Don’t communicate by sharing memory, share memory by communicating”。Actor模型和線程的共享內存機制是等價的。實際上,Actor模型一般通過底層的thread/lock/buffer 等機制來實現,是高層的機制。Actor模型是數學上的模型,有理論的支持。另一個類似的數學模型是CSP(communicating sequential process)。早期的實現這些理論的語言最著名的就是erlang和occam。尤其是erlang,所謂的Ericsson Language,目的就是實現大規模的併發程序,用於電信系統。Erlang後來成爲比較流行的語言。
類似Actor/CSP的消息傳遞機制。Go語言中也提供了這樣的功能。Go的併發實體叫做goroutine,類似coroutine,但不需要自己調度。Runtime自己就會把goroutine調度到系統的線程上去運行,多個goroutine共享一個線程。如果有一個要阻塞,系統就會自動把其他的goroutine調度到其他的線程上去。
一些名詞定義:
1. Processes, threads, green threads, protothreads, fibers, coroutines: what's the difference?
• Process: OS-managed (possibly) truly concurrent, at least in the presence of suitable hardware support. Exist within their own address space.
• Thread: OS-managed, within the same address space as the parent and all its other threads. Possibly truly concurrent, and multi-tasking is pre-emptive.
• Green Thread: These are user-space projections of the same concept as threads, but are not OS-managed. Probably not truly concurrent, except in the sense that there may be multiple worker threads or processes giving them CPU time concurrently, so probably best to consider this as interleaved or multiplexed.
• Protothreads: I couldn't really tease a definition out of these. I think they are interleaved and program-managed, but don't take my word for it. My sense was that they are essentially an application-specific implementation of the same kind of "green threads" model, with appropriate modification for the application domain.
• Fibers: OS-managed. Exactly threads, except co-operatively multitasking, and hence not truly concurrent.
• Coroutines: Exactly fibers, except not OS-managed.
Coroutines are computer program components that generalize subroutines to allow multiple entry points for suspending and resuming execution at certain locations. Coroutines are well-suited for implementing more familiar program components such as cooperative tasks, iterators, infinite lists and pipes.
• Continuation: An abstract representation of the control state of a computer program.
A continuation reifies the program control state, i.e. the continuationis a data structure that represents the computational process at a given point in the process' execution; the created data structure can be accessed by the programming language, instead of being hidden in the runtime environment. Continuations are useful for encoding other control mechanisms in programming languages such as exceptions, generators, coroutines, and so on.
The "current continuation" or "continuation of the computation step" is the continuation that, from the perspective of running code, would be derived from the current point in a program's execution. The term continuations can also be used to refer to first-class continuations, which are constructs that give a programming language the ability to save the execution state at any pointand return to that point at a later point in the program.(yield keywork in some languages, such as c# or python)
•Goroutines: They claim to be unlike anything else, but they seem to be exactly green threads, as in, process-managed in a single address space and multiplexed onto system threads. Perhaps somebody with more knowledge of Go can cut through the marketing material.


Actor模型的提出和實現主要是爲了解決限制服務端應用高併發的難題:過度依靠操作系統內核所提供的API來實現高併發。這種內核API在高併發下會產生大量線程調度,過多用戶態與內核的Context切換會使系統性能線性下降。

Actor模型的實現關鍵有兩點: 
1. 基於運行時環境自己實現併發實體調度(例如Coroutine),從而避免大量的內核API調用,那麼Context切換也可以自然避免。
2. 避免使用共享內存來實現信息共享(上文有描述)。


作者:banq,sinaID49811 

出處:http://www.jdon.com/45516

發佈了38 篇原創文章 · 獲贊 33 · 訪問量 47萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章