表達過去、現在與將來:之將來(2)
前情提要
本文的第一章:表達過去、現在與將來:之將來(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: https://repl.it/@taowen/es2017-client
網路伺服器
到目前為止,我們已經看到了coroutine是如何計算,sleep以及調用外部服務的。現在,我們需要把coroutine變成一臺伺服器。我們在這裡只提供es2017的代碼,lua代碼和前面的網路客戶端的代碼是很類似的。
es2017 version: https://repl.it/@taowen/es2017-server
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: https://jsfiddle.net/taowen/L0p516xv/56/
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是如何渲染的,表單是如何提交的,這些實現細節和本文主題無關。你可以在這裡 https://jsfiddle.net/taowen/L0p516xv/56/ 實際動手玩一下看看。
scheduler.user_input 是寫成了通用工具的形式。它可以在業務邏輯無關的前提下被複用。也就是把主動過程的辭彙庫進行了擴充,這樣有更多種更複雜的「future」可以用coroutine來表達。
Coroutine 休眠
我們還無法把coroutine UI投入使用,因為它有一個嚴重的問題。coroutine要在整個過程種都保持存活於計算機的內存中。如果用戶要明天才點按鈕,那麼就要保持流程存活一整天。我們需要讓狀態「休眠」到磁碟,所以我們可以在繼續執行之前節省資源。
這個例子需要用一個定製版本的lua解釋器:https://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去「調用」界面和資料庫,有點太過酷炫。但是這是一種可行的表述未來的方式,而且有的時候能夠派上大用場。
推薦閱讀: