前情提要

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

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

主動過程

在前面的簡單過程的例子裏,coroutine是被動的。它等待未來的指令,怎麼被告知就怎麼執行。這和需求文檔的表述方式是想反的,文檔描述事情的方式一般是以主動的語氣來描述。它知道當前在幹什麼,也知道接下來要幹什麼。

在本章中,我們將探索幾種用更主動的方式來「表述未來」。

Time scheduler

第一個例子是sleep

lua version: https://tio.run/

es2017 version:https://tio.run/

Call with continuation

coroutine.yield(sleep, {seconds=2}) 這寫法很笨拙。為什麼sub1_task不能直接把自己置為sleep模式呢?因為它沒法引用自己。一些語言提供了「call with continuation」,這種語法使得coroutine可以把接下來要做的計算變成一個continuation,然後傳遞給另外一個函數做為回調。我們可以在lua中模擬一下

lua version: https://tio.run/

這裡引入了「Future」的概念,它把task的continuation和它的需求封裝到了一起。當需求被滿足了之後,task就會被繼續執行。

Async 和 await

前面我們還沒有提供es2017的例子。因為es2017內置了自己版本的「future」。

es2017 version: https://tio.run/

雖然代碼看起來很不一樣。但是底層的機制實際上是很類似的。es2017的編譯器或者解釋器會把「await」翻譯成一個continuation,傳遞給promise。

我們可以把前面的lua代碼改得和javascript promise類似。它就是一個函數以「resolve」為參數,捕捉了task的continuation,然後把這個continuation做為回調註冊到某處,然後把代碼的執行權力交走。

網路客戶端

時間的scheduler是簡單的。它不需要給task提供返回值。如果coroutine需要扮演網路客戶端的角色。它需要一種拿到結果的機制。

lua version: https://tio.run/

看來提供返回值並不難做到。caller和callee共享了一個「future」對象,所以他們可以通過這個對象來互相發送請求和響應。用es2017的await語法,給coroutine提供返回值就更簡單了。

es2017 version: repl.it/@taowen/es2017-

網路伺服器

到目前為止,我們已經看到了coroutine是如何計算,sleep以及調用外部服務的。現在,我們需要把coroutine變成一臺伺服器。我們在這裡只提供es2017的代碼,lua代碼和前面的網路客戶端的代碼是很類似的。

es2017 version: repl.it/@taowen/es2017-

client和server的區別是,對於server,它需要兩次await,但是對於客戶端,只需要一次await。第一次是等待從scheduler拿到請求,第二次是把響應發送回去。對於TCP連接來說,寫入也可能因為內核的緩存區滿了而阻塞。所以即便是發迴響應,也應該是一個非同步操作。

被動過程 V.S. 主動過程

我們已經看到了好幾個主動過程的例子了。它可以做下面這些事情:

  • sleep
  • 做為客戶端調用其他的服務
  • 做為伺服器被其他服務調用

對比前面一章中的被動過程,主動過程還真是主動的。它似乎能夠控制自己的命運。我們這裡可以直觀比較一下。有兩個步驟,在步驟之間需要sleep一秒的時間,兩種寫法分別是這樣的:

Passive version: https://tio.run/

Active version: https://tio.run/

從「主動」v.s. 「被動」的角度來說,兩者真的沒有多大區別。從 sub1_task 的角度來看,被動的版本直接使用了 "coroutine.yield",等待 scheduler 把自己喚醒。而主動的版本使用了「sleep」,但是內部還是使用了「coroutine.yield」來等待 scheduler。兩個版本本質上乾的事情是一樣的,他們調用了」yield「把自己掛起了。

區別在於預先約定的協議。主動版本在scheduler和task之間有一個明確的協議。如果task把自己掛起到了」sleep_future「裏,scheduler就有責任在指定的時間把它重新喚醒。正如你使用 os.execute("sleep 1") 一樣,操作系統有責任把進程在1秒鐘之後喚醒。

主動過程的優勢在於,這麼搞之後scheduler就變成了基礎設施了,它是不含有業務邏輯的。所有不穩定的業務邏輯都隔離到了task裏了。所以改代碼一般改一個地方就可以了。對於被動過程而言,如果流程需要更新,sub1_task 和 scheduler 都需要修改。

scheduler 就變成了一個開放的平臺,而tasks就是插件插到這個平臺上來複用平臺的能力。

Coroutine UI

掛起在 Scheduler 裏的 task 是匿名的。這是一個問題。如果我們不知道你在等什麼,我怎麼知道給什麼呢?我們知道 sub1_task 希望知道下一步是 step1_add 還是 step1_sub。這個本質上是一個」用戶界面的問題「。

我們希望描述這樣的過程

當task被啟動之後,它就會給用戶兩個選擇,要麼是add要麼是sub。用戶選擇之後計算的結果會被回顯。一秒鐘之後,UI會切換回去重新讓用戶選擇兩個選項,然後做第二次的計算。這個過程無限往複。

這個過程可以被用這份代碼表達

es2017 version: jsfiddle.net/taowen/L0p

scheduler可以暴露」sleep「,「recv」,「reply」這樣的API,它也可以提供「user_input」這樣的API做為可復用的基礎設施。當然,scheduler無法知道用戶界面在不同流程下應該是怎樣的,所以它需要參數來指定使用什麼 UI 組件,使用什麼樣的 model 來渲染 view。這裡是 scheduler 的代碼:

The user_input the render the view by setting three variables:

user_input 通過設置下面三個變數來渲染 view:

  • step_ui: 使用哪個 UI 組件
  • step_ui_input: 界面的輸入數據
  • step_callback: 用戶提交表單之後的回調

我們已經學習過了,「await」,「promise」和「resolve」是如何工作的。很顯然,當用戶提交了表單之後,resolve會被回調,從而繼續了sub1_task的執行。實際view是如何渲染的,表單是如何提交的,這些實現細節和本文主題無關。你可以在這裡 jsfiddle.net/taowen/L0p 實際動手玩一下看看。

scheduler.user_input 是寫成了通用工具的形式。它可以在業務邏輯無關的前提下被複用。也就是把主動過程的辭彙庫進行了擴充,這樣有更多種更複雜的「future」可以用coroutine來表達。

Coroutine 休眠

我們還無法把coroutine UI投入使用,因為它有一個嚴重的問題。coroutine要在整個過程種都保持存活於計算機的內存中。如果用戶要明天才點按鈕,那麼就要保持流程存活一整天。我們需要讓狀態「休眠」到磁碟,所以我們可以在繼續執行之前節省資源。

這個例子需要用一個定製版本的lua解釋器:github.com/fnuecke/eris

我們進一步擴展了scheduler。這次「hibernate」函數使得task可以把自己保存到磁碟上,那麼很長期的「future」都可以用一個完整的coroutine「fib」來描述。

Coroutine 資料庫

但是休眠這個實現也有嚴重的問題。「continuation.data」是用一種詭異的二進位格式實現的。用「order」對象來表達流程就不會有這樣的問題。我們一般會用在資料庫裏創建「order」表的方式來保存尚未完成的order流程。

如果用coroutine來代表這樣的訂單流程,大概是這樣的形式:

訂單表的order_status欄位的值相當於一個遊標,跟蹤了執行到哪裡了。它本質上和coroutine裏的「coroutine.yield」的位置是一個東西。所以我們可以用lua的label來表達order_status的概念。在持久化coroutine的時候,「program counter」可以被翻譯為「order_status」,然後映射為一個資料庫的欄位。

Coroutine execution

Yield cut the coroutine into pieces. Each piece can be executed in a different place. As es2017 and lua are both single threaded. In other multi-thread languages, such as kotlin, the coroutine can be passed around from thread to thread.

A side-effect of coroutine hibernation is we are able to move the computation itself.

The continuation.data in the previous example can be moved to a different machine, and resume execution at that machine. Being able to move computation will be very handy, in two cases

  • The data is too big to move, such as map/reduce data stream
  • Need to manipulate resource local to that machine, such as a deployment script would want to start a process on that machine

Existing coroutine implementation does not have the facility to specify where to execute, because most language only concerns a single os process alive in a single machine.

表達力小結

我們在主動過程中可以表達計算,表達UI,甚至表達對資料庫的需求。coroutine把執行權yield給scheduler,從而復用預先定義的穩定的原子操作。所有不穩定的業務邏輯可以被一個coroutine完整包裹起來。

使用主動過程來表達「未來會發生什麼」這樣的業務邏輯是反常規的。看著一個coroutine去「調用」界面和資料庫,有點太過酷炫。但是這是一種可行的表述未來的方式,而且有的時候能夠派上大用場。

推薦閱讀:

相關文章