作者:劉小溪,Maxleap的高級(jí)開(kāi)發(fā)工程師,喜歡倒騰一些有意思的技術(shù)框架,對(duì)新的技術(shù)以及語(yǔ)言非常有興趣,以前在shopex擔(dān)任架構(gòu)師,目前在Maxleap負(fù)責(zé)基礎(chǔ)架構(gòu)以及服務(wù)框架這塊技術(shù),同時(shí)也會(huì)對(duì)Vert.x的社區(qū)提供一些開(kāi)源上的支持。
責(zé)編: 錢(qián)曙光,關(guān)注架構(gòu)和算法領(lǐng)域,尋求報(bào)道或者投稿請(qǐng)發(fā)郵件qianshg@csdn.net,另有「CSDN 高級(jí)架構(gòu)師群」,內(nèi)有諸多知名互聯(lián)網(wǎng)公司的大牛架構(gòu)師,歡迎架構(gòu)師加微信qshuguang2008申請(qǐng)入群,備注姓名+公司+職位。
這東西其實(shí)有很多名詞,比如有的人喜歡稱為纖程(Fiber),或者綠色線程(GreenThread)。其實(shí)最直觀的解釋可以定義為線程的線程。有點(diǎn)拗口,但本質(zhì)上就是這樣。
我們先回憶一下線程的定義,操作系統(tǒng)產(chǎn)生一個(gè)進(jìn)程,進(jìn)程再產(chǎn)生若干個(gè)線程并行
的處理邏輯,線程的切換由操作系統(tǒng)負(fù)責(zé)調(diào)度。傳統(tǒng)語(yǔ)言C++ Java等線程其實(shí)與操作系統(tǒng)線程是1:1的關(guān)系,每個(gè)線程都有自己的Stack,Java在64位系統(tǒng)默認(rèn)Stack大小是1024KB,所以指望一個(gè)進(jìn)程開(kāi)啟上萬(wàn)個(gè)線程是不現(xiàn)實(shí)的。但是實(shí)際上我們也不會(huì)這么干,因?yàn)槠疬@么多線程并不能充分的利用CPU,大部分線程處于等待狀態(tài),CPU也沒(méi)有這么核讓線程使用。所以一般線程數(shù)目都是CPU的核數(shù)。
傳統(tǒng)的J2EE系統(tǒng)都是基于每個(gè)請(qǐng)求占用一個(gè)線程去完成完整的業(yè)務(wù)邏輯(包括事務(wù))。所以系統(tǒng)的吞吐能力取決于每個(gè)線程的操作耗時(shí)。如果遇到很耗時(shí)的I/O行為,則整個(gè)系統(tǒng)的吞吐立刻下降,比如JDBC是同步阻塞的,這也是為什么很多人都說(shuō)數(shù)據(jù)庫(kù)是瓶頸的原因。這里的耗時(shí)其實(shí)是讓CPU一直在等待I/O返回,說(shuō)白了線程根本沒(méi)有利用CPU去做運(yùn)算,而是處于空轉(zhuǎn)狀態(tài)。暴殄天物啊。另外過(guò)多的線程,也會(huì)帶來(lái)更多的ContextSwitch開(kāi)銷(xiāo)。
Java的JDK里有封裝很好的ThreadPool,可以用來(lái)管理大量的線程生命周期,但是本質(zhì)上還是不能很好的解決線程數(shù)量的問(wèn)題,以及線程空轉(zhuǎn)占用CPU資源的問(wèn)題。
先階段行業(yè)里的比較流行的解決方案之一就是單線程加上異步回調(diào)。其代表派是node.js
以及Java里的新秀Vert.x
。他們的核心思想是一樣的,遇到需要進(jìn)行I/O操作的地方,就直接讓出CPU資源,然后注冊(cè)一個(gè)回調(diào)函數(shù),其他邏輯則繼續(xù)往下走,I/O結(jié)束后帶著結(jié)果向事件隊(duì)列里插入執(zhí)行結(jié)果,然后由事件調(diào)度器調(diào)度回調(diào)函數(shù),傳入結(jié)果。這時(shí)候執(zhí)行的地方可能就不是你原來(lái)的代碼區(qū)塊了,具體表現(xiàn)在代碼層面上,你會(huì)發(fā)現(xiàn)你的局部變量全部丟失,畢竟相關(guān)的棧已經(jīng)被覆蓋了,所以為了保存之前的棧上數(shù)據(jù),你要么選擇帶著一起放入回調(diào)函數(shù)里,要么就不停的嵌套,從而引起反人類的Callback hell。
因此相關(guān)的Promise,CompletableFuture等技術(shù)都是為解決相關(guān)的問(wèn)題而產(chǎn)生的。但是本質(zhì)上還是不能解決業(yè)務(wù)邏輯的割裂。
說(shuō)了這么多,終于可以提一下協(xié)程了,協(xié)程的本質(zhì)上其實(shí)還是和上面的方法一樣,只不過(guò)他的核心點(diǎn)在于調(diào)度那塊由他來(lái)負(fù)責(zé)解決,遇到阻塞操作,立刻yield掉,并且記錄當(dāng)前棧上的數(shù)據(jù),阻塞完后立刻再找一個(gè)線程恢復(fù)棧并把阻塞的結(jié)果放到這個(gè)線程上去跑,這樣看上去好像跟寫(xiě)同步代碼沒(méi)有任何差別,這整個(gè)流程可以稱為coroutine
,而跑在由coroutine負(fù)責(zé)調(diào)度的線程稱為Fiber
。比如Golang里的 go
關(guān)鍵字其實(shí)就是負(fù)責(zé)開(kāi)啟一個(gè)Fiber
,讓func
邏輯跑在上面。而這一切都是發(fā)生的用戶態(tài)上,沒(méi)有發(fā)生在內(nèi)核態(tài)上,也就是說(shuō)沒(méi)有ContextSwitch上的開(kāi)銷(xiāo)。
既然我們的標(biāo)題叫Java里的協(xié)程,自然我們會(huì)討論JVM上的實(shí)現(xiàn),JVM上早期有kilim
以及現(xiàn)在比較成熟的Quasar
。而本文章會(huì)全部基于Quasar
,因?yàn)?code>kilim已經(jīng)很久不更新了。
上面已經(jīng)說(shuō)明了什么是Fiber
,什么是coroutine
。這里嘗試通過(guò)Quasar
來(lái)實(shí)現(xiàn)類似于golang的coroutine
以及channel
。這里假設(shè)各位已經(jīng)大致了解golang。
為了對(duì)比,這里先用golang實(shí)現(xiàn)一個(gè)對(duì)于10以內(nèi)自然數(shù)分別求平方的例子,當(dāng)然了可以直接單線程for循環(huán)就完事了,但是為了凸顯coroutine的高逼格,我們還是要稍微復(fù)雜化一點(diǎn)的。
func counter(out chan<- int) { for x := 0; x < 10; x++ { out <- x } close(out)}func squarer(out chan<- int, in <-chan int) { for v := range in { out <- v * v } close(out)}func printer(in <-chan int) { for v := range in { fmt.Println(v) }}func main() { //定義兩個(gè)int類型的channel naturals := make(chan int) squares := make(chan int) //產(chǎn)生兩個(gè)Fiber,用go關(guān)鍵字 go counter(naturals) go squarer(squares, naturals) //獲取計(jì)算結(jié)果 printer(squares)}
上面的例子,有點(diǎn)類似生產(chǎn)消費(fèi)者模式,通過(guò)channel兩解耦兩邊的數(shù)據(jù)共享。大家可以將channel理解為Java里的SynchronousQueue
。那傳統(tǒng)的基于線程模型的Java實(shí)現(xiàn)方式,想必大家都知道怎么做,這里就不啰嗦了,我直接上Quasar
版的,幾乎可以原封不動(dòng)的copy golang的代碼。
public class Example { private static void printer(Channel<Integer> in) throws SuspendExecution, InterruptedException { Integer v; while ((v = in.receive()) != null) { System.out.println(v); } } public static void main(String[] args) throws ExecutionException, InterruptedException, SuspendExecution { //定義兩個(gè)Channel Channel<Integer> naturals = Channels.newChannel(-1); Channel<Integer> squares = Channels.newChannel(-1); //運(yùn)行兩個(gè)Fiber實(shí)現(xiàn). new Fiber(() -> { for (int i = 0; i < 10; i++) naturals.send(i); naturals.close(); }).start(); new Fiber(() -> { Integer v; while ((v = naturals.receive()) != null) squares.send(v * v); squares.close(); }).start(); printer(squares); }}
看起來(lái)Java似乎要啰嗦一點(diǎn),沒(méi)辦法這是Java的風(fēng)格,而且畢竟不是語(yǔ)言上支持coroutine,是通過(guò)第三方的庫(kù)。到后面我會(huì)考慮用其他JVM上的語(yǔ)言去實(shí)現(xiàn),這樣會(huì)顯得更精簡(jiǎn)一點(diǎn)。
說(shuō)到這里各位肯定對(duì)Fiber很好奇了。也許你會(huì)表示懷疑Fiber是不是如上面所描述的那樣,下面我們嘗試用Quasar建立一百萬(wàn)個(gè)Fiber,看看內(nèi)存占用多少,我先嘗試了創(chuàng)建百萬(wàn)個(gè)Thread
。
for (int i = 0; i < 1_000_000; i++) { new Thread(() -> { try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } }).start();}
很不幸,直接報(bào)Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
,這是情理之中的。下面是通過(guò)Quasar
建立百萬(wàn)個(gè)Fiber
。
public static void main(String[] args) throws ExecutionException, InterruptedException, SuspendExecution { int FiberNumber = 1_000_000; CountDownLatch latch = new CountDownLatch(1); AtomicInteger counter = new AtomicInteger(0); for (int i = 0; i < FiberNumber; i++) { new Fiber(() -> { counter.incrementAndGet(); if (counter.get() == FiberNumber) { System.out.println("done"); } Strand.sleep(1000000); }).start(); } latch.await();}
我這里加了latch,阻止程序跑完就關(guān)閉,Strand.sleep
其實(shí)跟Thread.sleep
一樣,只是這里針對(duì)的是Fiber
。
最終控制臺(tái)是可以輸出done
的,說(shuō)明程序已經(jīng)創(chuàng)建了百萬(wàn)個(gè)Fiber,設(shè)置Sleep是為了讓Fiber
一直運(yùn)行,從而方便計(jì)算內(nèi)存占用。官方宣稱一個(gè)空閑的Fiber
大約占用400Byte
,那這里應(yīng)該是占用400MB
堆內(nèi)存,但是這里通過(guò)jmap -heap pid
顯示大約占用了1000MB
,也就是說(shuō)一個(gè)Fiber
占用1KB。
其實(shí)Quasar實(shí)現(xiàn)的coroutine的方式與Golang很像,只不過(guò)一個(gè)是框架級(jí)別實(shí)現(xiàn),一個(gè)是語(yǔ)言內(nèi)置機(jī)制而已。
如果你熟悉了Golang的調(diào)度機(jī)制,那理解Quasar的調(diào)度機(jī)制就會(huì)簡(jiǎn)單很多,因?yàn)閮烧呤遣畈欢嗟摹?/p>
Quasar里的Fiber其實(shí)是一個(gè)continuation,他可以被Quasar定義的scheduler調(diào)度,一個(gè)continuation記錄著運(yùn)行實(shí)例的狀態(tài),而且會(huì)被隨時(shí)中斷,并且也會(huì)隨后在他被中斷的地方恢復(fù)。Quasar其實(shí)是通過(guò)修改bytecode來(lái)達(dá)到這個(gè)目的,所以運(yùn)行Quasar程序的時(shí)候,你需要先通過(guò)java-agent在運(yùn)行時(shí)修改你的代碼,當(dāng)然也可以在編譯期間這么干。golang的內(nèi)置了自己的調(diào)度器,Quasar則默認(rèn)使用ForkJoinPool
這個(gè)JDK7以后才有的,具有work-stealing
功能的線程池來(lái)當(dāng)調(diào)度器。work-stealing非常重要,因?yàn)槟悴磺宄膫€(gè)Fiber會(huì)先執(zhí)行完,而work-stealing可以動(dòng)態(tài)的從其他的等等隊(duì)列偷一個(gè)context過(guò)來(lái),這樣可以最大化使用CPU資源。
那這里你會(huì)問(wèn)了,Quasar怎么知道修改哪些字節(jié)碼呢,其實(shí)也很簡(jiǎn)單,Quasar會(huì)通過(guò)java-agent在運(yùn)行時(shí)掃描哪些方法是可以中斷的,同時(shí)會(huì)在方法被調(diào)用前和調(diào)度后的方法內(nèi)插入一些continuation
邏輯,如果你在方法上定義了@Suspendable
注解,那Quasar會(huì)對(duì)調(diào)用該注解的方法做類似下面的事情。
這里假設(shè)你在方法f
上定義了@Suspendable
,同時(shí)去調(diào)用了有同樣注解的方法g
,那么所有調(diào)用f
的方法會(huì)插入一些字節(jié)碼,這些字節(jié)碼的邏輯就是記錄當(dāng)前Fiber棧上的狀態(tài),以便在未來(lái)可以動(dòng)態(tài)的恢復(fù)。(Fiber類似線程也有自己的棧)。在suspendable方法鏈內(nèi)
Fiber的父類會(huì)調(diào)用Fiber.park
,這樣會(huì)拋出SuspendExecution
異常,從而來(lái)停止線程
的運(yùn)行,好讓Quasar的調(diào)度器執(zhí)行調(diào)度。這里的SuspendExecution
會(huì)被Fiber自己捕獲,業(yè)務(wù)層面上不應(yīng)該捕獲到。如果Fiber被喚醒了(調(diào)度器層面會(huì)去調(diào)用Fiber.unpark
),那么f
會(huì)在被中斷的地方重新被調(diào)用(這里Fiber會(huì)知道自己在哪里被中斷),同時(shí)會(huì)把g
的調(diào)用結(jié)果(g
會(huì)return結(jié)果)插入到f
的恢復(fù)點(diǎn),這樣看上去就好像g
的return是f
的local variables
了,從而避免了callback嵌套。
上面啰嗦了一大堆,其實(shí)簡(jiǎn)單點(diǎn)講就是,想辦法讓運(yùn)行中的線程棧停下來(lái),好讓Quasar的調(diào)度器介入。JVM線程中斷的條件只有兩個(gè),一個(gè)是拋異常,另外一個(gè)就是return。這里Quasar就是通過(guò)拋異常的方式來(lái)達(dá)到的,所以你會(huì)看到我上面的代碼會(huì)拋出SuspendExecution
。但是如果你真捕獲到這個(gè)異常,那就說(shuō)明有問(wèn)題了,所以一般會(huì)這么寫(xiě)。
@Suspendablepublic int f() { try { // do some stuff return g() * 2; } catch(SuspendExecution s) { //這里不應(yīng)該捕獲到異常. throw new AssertionError(s); }}
在github上無(wú)意中發(fā)現(xiàn)一個(gè)有趣的benchmark,大致是測(cè)試各種語(yǔ)言在生成百萬(wàn)actor/Fiber的開(kāi)銷(xiāo)skynet。
大致的邏輯是先生成10個(gè)Fiber,每個(gè)Fiber再生成10個(gè)Fiber,直到生成1百萬(wàn)個(gè)Fiber,然后每個(gè)Fiber做加法累積計(jì)算,并把結(jié)果發(fā)到channel里,這樣一直遞歸到根Fiber。后將最終結(jié)果發(fā)到channel。如果邏輯沒(méi)有錯(cuò)的話結(jié)果應(yīng)該是499999500000。我們搞個(gè)Quasar版的,來(lái)測(cè)試一下性能。
所有的測(cè)試都是基于我的Macbook Pro Retina 2013later。Quasar-0.7.5:JDK8,JDK 1.8.0_91,Golang 1.6
public class Skynet { private static final int RUNS = 4; private static final int BUFFER = 1000; // = 0 unbufferd, > 0 buffered ; < 0 unlimited static void skynet(Channel<Long> c, long num, int size, int div) throws SuspendExecution, InterruptedException { if (size == 1) { c.send(num); return; } Channel<Long> rc = newChannel(BUFFER); long sum = 0L; for (int i = 0; i < div; i++) { long subNum = num + i * (size / div); new Fiber(() -> skynet(rc, subNum, size / div, div)).start(); } for (int i = 0; i < div; i++) sum += rc.receive(); c.send(sum); } public static void main(String[] args) throws Exception { //這里跑4次,是為了讓JVM預(yù)熱好做優(yōu)化,所以我們以最后一個(gè)結(jié)果為準(zhǔn)。 for (int i = 0; i < RUNS; i++) { long start = System.nanoTime(); Channel<Long> c = newChannel(BUFFER); new Fiber(() -> skynet(c, 0, 1_000_000, 10)).start(); long result = c.receive(); long elapsed = (System.nanoTime() - start) / 1_000_000; System.out.println((i + 1) + ": " + result + " (" + elapsed + " ms)"); } }}
golang的代碼我就不貼了,大家可以從github上拿到,我這里直接貼出結(jié)果。
platform | time |
---|---|
Golang | 261ms |
Quasar | 612ms |
從Skynet測(cè)試中可以看出,Quasar的性能對(duì)比Golang還是有差距的,但是不應(yīng)該達(dá)到兩倍多吧,經(jīng)過(guò)向Quasar作者求證才得知這個(gè)測(cè)試并沒(méi)有測(cè)試出實(shí)際性能,只是測(cè)試調(diào)度開(kāi)銷(xiāo)而已。
因?yàn)閟kynet方法內(nèi)部幾乎沒(méi)有做任何事情,只是簡(jiǎn)單的做了一個(gè)加法然后進(jìn)一步的遞歸生成新的Fiber而已,相當(dāng)于只是測(cè)試了Quasar生成并調(diào)度百萬(wàn)Fiber所需要的時(shí)間而已。而Java里的加法操作開(kāi)銷(xiāo)遠(yuǎn)比生成Fiber的開(kāi)銷(xiāo)要低,因此感覺(jué)整體性能不如golang(golang的coroutine是語(yǔ)言級(jí)別的)。
實(shí)際上我們?cè)趯?shí)際項(xiàng)目中生成的Fiber中不可能只做一下簡(jiǎn)單的加法就退出,至少要花費(fèi)1ms做一些簡(jiǎn)單的事情吧,(Quasar里Fiber的調(diào)度差不多在us級(jí)別),所以我們考慮在skynet里加一些比較耗時(shí)的操作,比如隨機(jī)生成1000個(gè)整數(shù)并對(duì)其進(jìn)行排序,這樣Fiber里算是有了相應(yīng)的性能開(kāi)銷(xiāo),與調(diào)度的開(kāi)銷(xiāo)相比,調(diào)度的開(kāi)銷(xiāo)就可以忽略不計(jì)了。(大家可以把調(diào)度開(kāi)銷(xiāo)想象成不定積分的常數(shù))。
下面我分別為兩種語(yǔ)言了加了數(shù)組排序邏輯,并插在響應(yīng)的Fiber里。
public class Skynet { private static Random random = new Random(); private static final int NUMBER_COUNT = 1000; private static final int RUNS = 4; private static final int BUFFER = 1000; // = 0 unbufferd, > 0 buffered ; < 0 unlimited private static void numberSort() { int[] nums = new int[NUMBER_COUNT]; for (int i = 0; i < NUMBER_COUNT; i++) nums[i] = random.nextInt(NUMBER_COUNT); Arrays.sort(nums); } static void skynet(Channel<Long> c, long num, int size, int div) throws SuspendExecution, InterruptedException { if (size == 1) { c.send(num); return; } //加入排序邏輯 numberSort(); Channel<Long> rc = newChannel(BUFFER); long sum = 0L; for (int i = 0; i < div; i++) { long subNum = num + i * (size / div); new Fiber(() -> skynet(rc, subNum, size / div, div)).start(); } for (int i = 0; i < div; i++) sum += rc.receive(); c.send(sum); } public static void main(String[] args) throws Exception { for (int i = 0; i < RUNS; i++) { long start = System.nanoTime(); Channel<Long> c = newChannel(BUFFER); new Fiber(() -> skynet(c, 0, 1_000_000, 10)).start(); long result = c.receive(); long elapsed = (System.nanoTime() - start) / 1_000_000; System.out.println((i + 1) + ": " + result + " (" + elapsed + " ms)"); } }}
const ( numberCount = 1000 loopCount = 1000000)//排序函數(shù)func numberSort() { nums := make([]int, numberCount) for i := 0; i < numberCount; i++ { nums[i] = rand.Intn(numberCount) } sort.Ints(nums)}func skynet(c chan int, num int, size int, div int) { if size == 1 { c <- num return } //加了排序邏輯 numberSort() rc := make(chan int) var sum int for i := 0; i < div; i++ { subNum := num + i*(size/div) go skynet(rc, subNum, size/div, div) } for i := 0; i < div; i++ { sum += <-rc } c <- sum}func main() { c := make(chan int) start := time.Now() go skynet(c, 0, loopCount, 10) result := <-c took := time.Since(start) fmt.Printf("Result: %d in %d ms.\n", result, took.Nanoseconds()/1e6)}
platform | time |
---|---|
Golang | 23615ms |
Quasar | 15448ms |
最后再進(jìn)行一次測(cè)試,發(fā)現(xiàn)Java的性能優(yōu)勢(shì)體現(xiàn)出來(lái)了。幾乎是golang的1.5倍,這也許是JVM/JDK經(jīng)過(guò)多年優(yōu)化的優(yōu)勢(shì)。因?yàn)榧恿藰I(yè)務(wù)邏輯后,對(duì)比的就是各種庫(kù)以及編譯器對(duì)語(yǔ)言的優(yōu)化了,協(xié)程調(diào)度開(kāi)銷(xiāo)幾乎可以忽略不計(jì)。
其實(shí)早在JDK1的時(shí)代,Java的線程被稱為GreenThread
,那個(gè)時(shí)候就已經(jīng)有了Fiber,但是當(dāng)時(shí)不能與操作系統(tǒng)實(shí)現(xiàn)N:M綁定,所以放棄了?,F(xiàn)在Quasar憑借ForkJoinPool
這個(gè)成熟的線程調(diào)度庫(kù)。另外,如果你希望你的代碼能夠跑在Fiber里面,需要一個(gè)很大的前提條件,那就是你所有的庫(kù),必須是異步無(wú)阻塞的,也就說(shuō)必須類似于node.js上的庫(kù),所有的邏輯都是異步回調(diào),而自Java里基本上所有的庫(kù)都是同步阻塞的,很少見(jiàn)到異步無(wú)阻塞的。而且得益于J2EE,以及Java上的三大框架(SSH)洗腦,大部分Java程序員都已經(jīng)習(xí)慣了基于線程,線性的完成一個(gè)業(yè)務(wù)邏輯,很難讓他們接受一種將邏輯割裂的異步編程模型。
但是隨著異步無(wú)阻塞這股風(fēng)氣起來(lái),以及相關(guān)的coroutine
語(yǔ)言Golang大力推廣,人們?cè)絹?lái)越知道如何更好的榨干CPU性能(讓CPU避免不必要的等待,減少上下文切換),阻塞的行為基本發(fā)生在I/O上,如果能有一個(gè)庫(kù)能把所有的I/O行為都包裝成異步阻塞的話,那么Quasar就會(huì)有用武之地,JVM上公認(rèn)的是異步網(wǎng)絡(luò)通信庫(kù)是Netty,通過(guò)Netty基本解決了網(wǎng)絡(luò)I/O問(wèn)題,另外還有一個(gè)是文件I/O,而這個(gè)JDK7提供的NIO2就可以滿足,通過(guò)AsynchronousFileChannel
即可。剩下的就是如何將他們封裝成更友好的API了。目前能達(dá)到生產(chǎn)級(jí)別的這種異步工具庫(kù),JVM上只有Vert.x3
,封裝了Netty4,封裝了AsynchronousFileChannel
,而且Vert.x官方也出了一個(gè)相對(duì)應(yīng)的封裝了Quasar
的庫(kù)vertx-sync
。
Quasar目前是由一家商業(yè)公司Parallel Universe控制著,且有自己的一套體系,包括Quasar-actor,Quasar-galaxy等各個(gè)模塊,但是Quasar-core是開(kāi)源的,此外Quasar自己也通過(guò)Fiber封裝了很多的第三方庫(kù),目前全都在comsat這個(gè)項(xiàng)目里。隨便找一個(gè)項(xiàng)目看看,你會(huì)發(fā)現(xiàn)其實(shí)通過(guò)Quasar的Fiber去封裝第三方的同步庫(kù)還是很簡(jiǎn)單的。
異步無(wú)阻塞的編碼方式其實(shí)有很多種實(shí)現(xiàn),比如node.js的提倡的Promise,對(duì)應(yīng)到Java8的就是CompletableFuture。
另外事件響應(yīng)式也算是一個(gè)比較流行的做法,比如ReactiveX系列,RxJava、Rxjs、RxSwift等。我個(gè)人覺(jué)得RxJava是一個(gè)非常好的函數(shù)式響應(yīng)實(shí)現(xiàn)(JDK9會(huì)有對(duì)應(yīng)的JDK實(shí)現(xiàn)),但是我們不能要求所有的程序員一眼就提煉出業(yè)務(wù)里的functor,monad(這些能力需要長(zhǎng)期浸淫在函數(shù)式編程思想里),反而RxJava特別適合用在前端與用戶交互的部分,因?yàn)橛脩舻狞c(diǎn)擊滑動(dòng)行為是一個(gè)個(gè)真實(shí)的事件流,這也是為什么RxJava在Android端非?;鸬脑?,而后端基本上都是通過(guò)Rest請(qǐng)求過(guò)來(lái),每一個(gè)請(qǐng)求其實(shí)已經(jīng)限定了業(yè)務(wù)范圍,不會(huì)再有復(fù)雜的事件邏輯,所以基本上RxJava在Vert.x這端只是做了一堆的flatmap,再加上微服務(wù)化,所有的業(yè)務(wù)邏輯都已經(jīng)做了最小的邊界,所以順序的同步的編碼方式更適合寫(xiě)業(yè)務(wù)邏輯的后端程序員。
所以這里Golang開(kāi)了個(gè)好頭,但是Golang也有其自身的限制,比如不支持泛型,當(dāng)然這個(gè)仁者見(jiàn)仁智者見(jiàn)智了,包的依賴管理比較弱,此外Golang沒(méi)有線程池的概念,如果coroutine里的邏輯發(fā)生了阻塞,那么整個(gè)程序會(huì)hang死。而這點(diǎn)Vert.x提供了一個(gè)Worker Pool的概念,可以將需要耗時(shí)執(zhí)行的邏輯包到線程池里面,執(zhí)行完后異步返回給EventLoop線程。
下一篇我們來(lái)研究一下vertx-sync
,讓vert.x里所有的異步編碼方式同步化,徹底解決Vert.x
里的Callback Hell。
聯(lián)系客服