前情提要

本文的第一章:表達過去、現在與將來:之將來(1)

本文的第二章:表達過去、現在與將來:之將來(2)

我們已經看到了單個coroutine可以有多麼強大。但是把所有的業務邏輯全部塞進一個coroutine是不可行的。從複雜度管理的角度,我們必須分而治之。而且「未來」也不可能是單線程的,我們必須描述同時進行的多個事情。

多任務

第一步是先要讓scheduler支持同時跑多個任務。操作系統是一個thread的scheduler一樣,它支持了執行比CPU數更多的線程。但是操作系統因為是一個底層的而且支持搶佔式調度的scheduler,所以相當之複雜。我們這裡要實現的只是一個非常簡單的合作式的scheduler。

lua version: https://tio.run/

我們把scheduler寫成通用的,我們要把tasks用一個lua table保存起來跑

lua version: https://tio.run/

再把sleep future和多任務結合起來,我們就有了這麼一個完整點的實現

lua vesion: https://tio.run/

es2017 await/async 內部有 ready_tasks。resolve 會把 task 放到這個隊列裏了。所以我們可以說 es2017 有一個內建的多任務 scheduler。

Decomposition(分解)

scheduler已經擴展為支持多個同時運行的任務了。下一步就是把一個業務流程分拆成多個子任務,從而能更好地管理複雜度。暫且不論task的粒度問題,有三種分拆長流程的方式:

這裡每一個框框都是一個coroutine。Orchestration(編排)是最簡單的形式:

lua version: https://tio.run/

Orchestration的好處是簡單。跟蹤A的調用鏈,我們可以瞭解一切。Orchestration的壞處是耦合。如果step2或者step3「不關你鳥事」,那麼要調用它們還要等它們返回是很煩的事情。在這種情況下Choreography會有幫助。

lua version: https://tio.run/

這種形式的 choreography 其實也沒有真正避免耦合。A還是知道B的存在的,B也是知道C的存在的。為瞭解耦這幾方,我們需要在它們之間插入一個存儲服務,例如消息隊列或者資料庫。

lua version: https://tio.run/

「A_subscribers」並不是一個很嚴謹的表達方式。消息隊列實際扮演的角色比這個要更複雜。我們在後面的章節會呈現一個更精確的表達。

Coordination(協調)

前面分解的目的僅僅是把一個線性的流程切分成更小的塊。看起來就是一個框接著一個框。和Decomposition不同,Coordination處理了框框之間有重疊的情況。籠統而言,可以分成兩種類型:

Fork/Join

Fork/join 其實仍然是 orchestration。假設A的老闆給A分配了一個任務,A把任務分拆成亮部分,然後分別交給了B和C去做。B和C同時去做任務,然後A在他們都完成了之後把結果收集回來,再報上去。在這個例子裏,B和C是負責的(responsible),A是擔責的(accountable)。orchestration的標誌性特徵就是有明確的責權(ownership),在最上面的傢伙需要擔下面所有的責任。

es2017 version: https://tio.run/

Fork/join 未必要等待所有的子任務結束。如果一個文檔需要一個VP的簽字。請求可以發給所有的VP,只要有一個批了就算批了,流程就可以繼續。

es2017 version: https://tio.run/

我們還可以接著問好幾個問題:

  • 如果需要在3個裡面等2個完成,怎麼搞?
  • 在 promise.race 返回之後,那個還在運行的coroutine就不管了嗎?可以取消掉麼?
  • 如果多個coroutine都等待同一個promise可以嗎?

溝通

另外一種協調(coordination)的方式是多個任務之間平等的協作。有效的協作是很有挑戰的事情。沒有老闆指揮的情況下,多個同級的團隊共同努力往往會走向混亂的局面。有效的協同需要建立在溝通的基礎上。

一個coroutine因為必須能夠「call」另外一個coroutine

es2017 version: https://tio.run/

A把自己停在了scheduler那裡。所以當B調用A的時候,scheduler可以把它喚醒。然而這個假設是server在client之前執行。如果B在A之前調用怎麼辦?這種情況下,我們需要能夠讓client把自己停在scheduler那裡。

完整的實現: github.com/dexscript/sc

現在我們有了在兩個coroutine之間做同步「RPC」的機制。為了識別server,我們把無名的coroutine升級成了有id的「actor」的概念。當前的實現是單線程的,因為javascript是單線程的。然而,「RPC」理論上可以擴展為調用實際運行在遠程機器上的actor。

這裡的「scheduler.stub」是一個語法糖,它讓call的寫法看起來很好看。

簡單總結一下到目前為止談到過的內容

  • Function/Object/Coroutine 可以被用來描述流程
  • Active process是一個和scheduler之間有明確協議的coroutine
  • Active process表達力很豐富,一個長業務流基本可以完全由其表述
  • Scheduler要支持合作式地多任務調度並不困難
  • Active process再次升級為「actor」,支持彼此之間 RPC

下一章中,我們要用actor來表達複雜的協作模式


推薦閱讀:
查看原文 >>
相關文章