协程和Java实现
多線程的性能問題:
1.同步鎖。
2.線程阻塞狀態和可運行狀態之間的切換。
3.線程上下文的切換。
協程,英文Coroutines,是一種比線程更加輕量級的存在。正如一個進程可以擁有多個線程一樣,一個線程也可以擁有多個協程。
協程,又稱微線程,纖程。英文名Coroutine。?
最大的優勢就是協程極高的執行效率。因為子程序切換不是線程切換,而是由程序自身控制,因此,沒有線程切換的開銷,和多線程比,線程數量越多,協程的性能優勢就越明顯。
第二大優勢就是不需要多線程的鎖機制,因為只有一個線程,也不存在同時寫變量沖突,在協程中控制共享資源不加鎖,只需要判斷狀態就好了,所以執行效率比多線程高很多。
因為協程是一個線程執行,那怎么利用多核CPU呢?最簡單的方法是多進程+協程,既充分利用多核,又充分發揮協程的高效率,可獲得極高的性能。
Lua語言
Lua從5.0版本開始使用協程,通過擴展庫coroutine來實現。
Python語言
正如剛才所寫的代碼示例,python可以通過 yield/send 的方式實現協程。在python 3.5以后,async/await 成為了更好的替代方案。
Go語言
Go語言對協程的實現非常強大而簡潔,可以輕松創建成百上千個協程并發執行。
早期版本的Go編譯器并不能很智能的發現和利用多核的優勢,即使在我們的代碼中創建了多個goroutine,但實際上所有這些goroutine都允許在同一個CPU上,在一個goroutine得到時間片執行的時候其它goroutine都會處于等待狀態。
實現下面的代碼可以顯式指定編譯器將goroutine調度到多個CPU上運行。
import "runtime"
...
runtime.GOMAXPROCS(4)
Java語言
如上文所說,Java語言并沒有對協程的原生支持,但是某些開源框架模擬出了協程的功能。
?
java協程框架----kilim實現機制解析
java語言處理多任務的模式是基于多線程,java語言級別原生并不支持協程,我們想要java語言支持協程,就需要在線程和協程之間架起一道橋梁。在某個事件點(我們成為掛起點)上,我們在應用級別備份當前任務在線程上的調用棧信息(包括局部變量和操作棧上的數據),釋放線程,讓它去執行下一個任務;等某些事件被觸發的時候,重新執行剛才的任務,用之前備份的調用棧信息恢復線程的調用棧,從掛起點開始執行。
?
Java如何實現協程
協程(Coroutine)這個詞其實有很多叫法,比如有的人喜歡稱為纖程(Fiber),或者綠色線程(GreenThread)。其實究其本質,對于協程最直觀的解釋是線程的線程。雖然讀上去有點拗口,但本質上就是這樣。
協程的核心在于調度那塊由他來負責解決,遇到阻塞操作,立刻放棄掉,并且記錄當前棧上的數據,阻塞完后立刻再找一個線程恢復棧并把阻塞的結果放到這個線程上去跑,這樣看上去好像跟寫同步代碼沒有任何差別,這整個流程可以稱為coroutine,而跑在由coroutine負責調度的線程稱為Fiber。
?
java協程的實現
早期,在JVM上實現協程一般會使用kilim,不過這個工具已經很久不更新了,現在常用的工具是Quasar,而本文章會全部基于Quasar來介紹。
下面嘗試通過Quasar來實現類似于go語言的coroutine以及channel。
Quasar是怎么實現Fiber的
其實Quasar實現的coroutine的方式與Go語言很像,只不過前者是使用框架來實現,而go語言則是語言內置的功能。
不過如果你熟悉了Go語言的調度機制的話,那么對于Quasar的調度機制就會好理解很多了,因為兩者有很多相似之處。
Quasar里的Fiber其實是一個continuation,他可以被Quasar定義的scheduler調度,一個continuation記錄著運行實例的狀態,而且會被隨時中斷,并且也會隨后在他被中斷的地方恢復。
Quasar其實是通過修改bytecode來達到這個目的,所以運行Quasar程序的時候,你需要先通過java-agent在運行時修改你的代碼,當然也可以在編譯期間這么干。go語言的內置了自己的調度器,而Quasar則是默認使用ForkJoinPool這個具有work-stealing功能的線程池來當調度器。work-stealing非常重要,因為你不清楚哪個Fiber會先執行完,而work-stealing可以動態的從其他的等等隊列偷一個context過來,這樣可以最大化使用CPU資源。
那這里你會問了,Quasar怎么知道修改哪些字節碼呢,其實也很簡單,Quasar會通過java-agent在運行時掃描哪些方法是可以中斷的,同時會在方法被調用前和調度后的方法內插入一些continuation邏輯,如果你在方法上定義了@Suspendable注解,那Quasar會對調用該注解的方法做類似下面的事情。
這里假設你在方法f上定義了@Suspendable,同時去調用了有同樣注解的方法g,那么所有調用f的方法會插入一些字節碼,這些字節碼的邏輯就是記錄當前Fiber棧上的狀態,以便在未來可以動態的恢復。(Fiber類似線程也有自己的棧)。在suspendable方法鏈內Fiber的父類會調用Fiber.park,這樣會拋出SuspendExecution異常,從而來停止線程的運行,好讓Quasar的調度器執行調度。這里的SuspendExecution會被Fiber自己捕獲,業務層面上不應該捕獲到。如果Fiber被喚醒了(調度器層面會去調用Fiber.unpark),那么f會在被中斷的地方重新被調用(這里Fiber會知道自己在哪里被中斷),同時會把g的調用結果(g會return結果)插入到f的恢復點,這樣看上去就好像g的return是f的local?variables了,從而避免了callback嵌套。
上面說了一大堆,其實簡單點來講就是,想辦法讓運行中的線程棧停下來,然后讓Quasar的調度器介入。
JVM線程中斷的條件有兩個:
1、拋異常
2、return。
而在Quasar中,一般就是通過拋異常的方式來達到的,所以你會看到上面的代碼會拋出SuspendExecution。
?
Coroutine in Java - Quasar Fiber實現?
Quasar Fiber則是通過字節碼修改技術在編譯或載入時織入必要的上下文保存/恢復代碼,通過拋異常來暫停,恢復的時候根據保存的上下文(Continuation),恢復jvm的方法調用棧和局部變量,Quasar Fiber提供相應的Java類庫來實現,對應用有一定的侵入性(很小)
Quasar Fiber 主要有 Instrument + Continuation + Scheduler幾個部分組成
-
Instrument?做一些代碼的植入,如park前后上下文的保存/恢復等
-
Continuation?保存方法調用的信息,如局部變量,引用等,用戶態的stack,這個也是跟akka等基于固定callback接口的異步框架最大的區別
-
Scheduler?調度器,負責將fiber分配到具體的os thread執行
下面具體介紹下Quasar Fiber的實現細節,最好先閱讀下quasar官方文檔
總結
- 上一篇: Jdk11,Jdk12的低延迟垃圾收集器
- 下一篇: 一个会画图的工程师