摘要:的科學定義是或者,它的標志性原語是。能解決一類對語言的實現來說特別無力的狀態機模型流程即狀態。容易實現是需要和的一個重要原因。
前面寫了一篇,寫的很粗,這篇講講一些細節。實際上Fiber/Coroutine vs Async/Await之爭不是一個簡單的continuation如何實現的問題,而是兩個完全不同的problem和solution domain。
Event Model我們回顧一下最純粹的Event Model。這曾經在UI編程,和現在仍然在microcontroller(MCU)編程中占據主力地位,在系統編程上是thread model了。
用MCU編程來講解最方便,在傳統UI編程上是一樣的。
單核的MCU具有硬件的Thread。
Main Thread是CPU的正常運行,Interrupt Thread(一般稱為ISR,Interrupt Service Routine)是硬件上的比Main Thread優先級更高的Thread,即所謂的搶先(pre-emptive)。
如果Main Thread在運行,有Interrupt進來,CPU會立刻跳轉到ISR入口執行,ISR原則上應該保存現場,運行,然后恢復現場,return。return之后Main Thread重新拿回CPU繼續運行。這里壓棧彈棧的細節不說了,這就是一個搶先CPU的過程。
在這種模式下編程,ISR里能訪問的變量,和Main Thread里訪問的變量,很明顯存在race,需要鎖機制。在系統級編程這種鎖機制就是lock,但是上述情況里Main Thread和ISR不是對稱的,所以做法略有區別:Main Thread里lock的辦法是禁止interrupt,然后開始執行critical section的代碼,完成后使能中斷;ISR里,如果不考慮ISR之間的搶先的話,不需要這個過程,因為它天生比Main Thread優先級高。
昨天我們說了業界都在把non-blocking叫做asynchronous;這里解釋一下asynchronous,asynchronous的正確含義是你寫了一個function,這個function在Main Thread和ISR里都能用,它叫做asynchronous function;如果是系統級編程,thread之間是對等的,它叫做thread-safe。
上述的模型在邏輯上沒問題,但是有兩個實踐上的麻煩:
asynchronous function不好寫,尤其是出現nested,在ISR有搶先的時候就更加麻煩;
禁止中斷的時間不能太長,太長的話會丟失中斷處理,邏輯上會出現問題;
ISR里的執行邏輯時間也不能太長,尤其不能等待什么,否則Main Thread會被block太久;
所以聰明人就有一個one-for-all的辦法:
在全局構造一個event queue;
任何ISR進來的時候,不去做邏輯處理,只把當時和中斷有關的狀態保存下來,構造一個event,壓入隊列;
在Main Thread里一個一個的取event,調用相應的event handler處理。
在這個模式下:
只有event queue是需要lock的;唯一的race發生在存取event的時候,存不用lock,取的時候禁止和使能中斷,這個時間不長,避免丟失中斷。
中斷的真正處理邏輯實際上發生在Main Thread,相當于deferred,它有延遲,但是不會亂序。
所有的代碼段都運行在Main Thread,沒有race,也就是人們最推崇event model的特性之一:run-to-completion。
它對于硬件搶先的多線程是個非常好的簡化。
Event / State Model那么有了Event,代碼模塊化的結果就是用State Machine建模。State Machine是一個理論上萬能的模型,任何代碼模塊你都可以給出state/event行為矩陣,稱為state transition table。它是對這個模塊的完備定義,也極具可測試性,也應用非常廣泛。OO編程的本質是State Machine建模,所有的method都可以看作是event,它可能引起Object的狀態遷移。在良好設計下,State具有邊界,即所謂的封裝。每個State都有owner,不是owner不能修改它,避免side-effect。
在實踐中,目前沒有任何一個流行語言能直接寫出簡潔的狀態機,尤其是狀態機組合;所以它存在于Pattern層面而不是語言層面;客觀的說這是計算工業的恥辱,但我們只能接受現狀。
IOIO不是象中斷一樣的自發事件。在通訊編程領域大家發明了一對名詞來描述這個問題:solicited和unsolicited;不算特別恰當但有總比沒有好。
IO是solicited,即有request才會有response;不需要request的那種自發event,是unsolicited event。
Event Model在處理這個問題上沒有理論上的障礙,你可以對調用函數對外界進行一個操作,然后得到結果時構造一個事件。
但是在實踐上,即使不考慮效率問題,這里仍然有一個大麻煩:在代碼層面上,執行request的地方在一個函數里,被某個event handler直接或間接調用,處理response的event handler在另一個地方。代碼的可讀性和可維護性都是完全沒有保障的,代碼質量取決于你的信仰、星座、美食愛好、或者性取向。
Continuation我們需要一些技術讓代碼看起來是人類的,不是AI或者外星人的,來對付IO問題。
更寬泛的說,Thread Model,Unix進程,Unix的一切皆IO哲學,Unix的open/read/write/close包打天下,就是我們在解決這類問題上的第一個大范圍成功的案例。但是Thread Model這個話題太大了,它還包括系統資源的虛擬化和物理多CPU的并發,所以我們不用這種擴大化的概念來討論。我們只討論兩個限定在單進程Event Model下的技術:coroutine和callback。
CoroutineCoroutine也是一個擴大化的概念。我們先討論廣的概念邊界,再來說它對io問題的解決辦法。
Coroutine的科學定義是stateful或者stackless function,它的標志性原語是yield。
注意原語(primitive)是理解一種編程語言或者編程技術的最關鍵點。如果一種技術致力于解決某個特定問題,它最好的辦法不是用pattern來解決,而是定義原語。OO語言有class,extends,implements;函數式語言允許function作為primitive value寫入assignment expression;以解決并發為目標的Go語言把channel作為標志。
yield是什么意思呢?它說的不是io,它說的是cpu,交出cpu。從這個意義上說,coroutine第一解決的問題是調度。它解決其他問題,包括timer/sleep,包括io,包括把一個整體計算切碎成很多單元來實現,都是靠yield。所以正確的表述是:Coroutine不是為特別解決io問題的設計,它首先解決cpu調度,它可以用于解決io問題。
第二點,為什么我們需要coroutine?它最擅長解決的問題是什么?
coroutine本質上仍然是一個event / state model,是一個object,但是不同的是,你不需要把所有的state都顯式表達出來,以對付continuation問題,coroutine允許開發者直接用語言提供的原始流程語句,來編碼state信息,你運行到哪里,這個時候整個coroutine內的local variable的組合,就是當前的state,每運行一次,就是一次state transition;和對象一樣它要從構造開始,到析構結束(return)。
coroutine能解決一類對OO語言的state pattern實現來說特別無力的狀態機模型:流程即狀態。如果你的狀態機model的是一個復雜流程,充滿條件分支、循環、和他們的嵌套,用coroutine寫起來非常簡單,而與之對應的狀態機,都不用寫代碼,定義transition table的時候程序員就要進醫院了。
coroutine對付io了嗎?yes and no。它是標準的Thread Model,thread model下io什么樣,它就什么樣了,no more, no less。
Callback, Promise, Async/Await這幾個貨本質上是一樣的,區別在形式上。當然很多時候形式很重要,但是我們先談本質。
const myFunction = (dirpath, callback) => { // do something // first io operation if (err) return callback(err) else return callback(null, entries) } // my code myFunction("/home/hello", (err, entires) => { // blah blah blah }) // do something else console.log("blah, blah...")
我們首先說callback的本質是一個event handler。調用myFunction相當于在前面說的最淳樸的event model里enqueue一個event,這個event的handler會根據event里定義的dirpath執行某個操作,操作結束的時候會構造另一個event,里面包含error或result。
這個純粹模型的寫法會非常復雜,從這個意義上說,node.js callback是一種簡單的continuation實現。
But wait! 兩者不是完全一致的!
myFunction函數里入口處do something部分的代碼;如果是我們上述的淳樸event model,它會在當前代碼結束之后執行,即console.log會先執行,等到全局的event manager開始層層dispatch event的時候,這個請求才可能landing到正確的handler,這段do something才開始執行,在console.log之后。
這是一個subtle,但是極為重要的區別。
插個話:callback形式如果在入口處do something立刻返回的話,對外部調用者來說是一場災難,因為它根本沒辦法確定它提供的callback在console.log之前還是之后執行。所以callback形式要guarantee它是異步的,用process.nextTick。promise和async/await在這個問題上是一大進步,它有異步保證,即使代碼形式上看起來是同步返回。
現在我們在自己腦袋上敲一錘子,昏過去,醒來的時候站在V8虛擬機的中控臺上。V8激進的inline函數來提高執行效率,在源碼層面上的myFunction函數調用,對V8編譯的代碼來說有一個call/return的邊界嗎?probably not!對編譯代碼來說,極大的可能性是執行函數邊界在myFunction內部第一個io處,而不是函數入口。
如果仍然用淳樸Event Model來類比,enqueue的event是一個純粹的io操作請求,而不是要執行myFunction函數!
所以寫到這里,一個關鍵的概念問題闡述清楚了:
coroutine is all about how to structure your control flow unit, while node callback is all about how to structure your io operation.
他們的出發點完全不同。
FP vs OO在建模層面(而不是語言技術層面)Funtional Programming,FP,它不是OO的對立,而是OO的超集。
在FP模型下,程序分為三個部分:Pure Functions,OO (state monads),和io (io monads)。
Pure的部分里,Pure Function只有輸入輸出(函數的輸入輸出,不是io輸入輸出),function和immutable數據結構是孿生姐妹。
OO的部分,如果程序需要state,OO至少在JavaScript里是絕對的最佳實踐,只有少量場合可以用閉包代替。
io的部分,應該多帶帶抽象出來,用callback、promise或者async/await做薄層封裝。
站在Pure Function的角度看,state和io都是它的外部世界。
Side EffectSide Effect一詞最廣的使用上指的是一個函數是不是pure。io function毫無疑問不pure,但是訪問state的呢?比如前面的代碼里,如果myFunction修改了它的調用者域內的閉包變量呢?這也是side effect。
在OO里我們保障減少side effect的影響的辦法,對于state(而不是io)范疇的變量來說,是用封裝原則來保障的。
在FP里對這個問題的有效辦法,則是immutable。
比如上面的代碼,如果你傳入myFunction的參數是一個對象,有深層次的結構,你會設計myFunction的函數約定是我要修改某個參數嗎?或者你會防止其他程序員這樣做嗎?
簡單的辦法就是用immutable來處理在pure function domain的這類問題,大家都用immutable;即使你沒有顯式的包含某些immutable庫,JavaScript里也有大量的集合類函數已經這樣做了。
LockLock分為兩類,atomic operation lock,和transactional lock。
transactional lock指的是一個操作的結果是all or none的,包括更新state,也包括執行output io操作。
容易實現transactional lock是需要fp和immutable的一個重要原因。因為它讓這種lock容易書寫。
Early Lock vs Opportunistic Lock你可以用一種鎖對付兩種情況。但是很難。用Big lock并發效率有問題,細粒度鎖編程難度大;而且對于JavaScript的單進程Event Model來說,用細粒度鎖對付transactional的數據完整性問題是overkill的。
另外一種鎖機制是Opportunistic lock,它和數據庫的事務操作是同樣的邏輯:你不斷的執行更新數據的操作,實際上是創建了一個副本,在最后commit的時候全部生效或失敗。如果失敗了可以重試這個過程。
在有immutable數據保證的情況下,如果有多步io操作導致更新過程分了幾個步驟,這個不是問題,你一直在創建一個副本,在最后需要更新state monad的時候,用referential equality check檢查input是否發生了變化(你也可以每一步都做,但幾率上說意義不大)。
這樣書寫事務問題,即使對文科生改行來的程序員來說也不算太難。
IO Lock在某些情況下IO操作的原子鎖無可替代;
比如你要更新一個文件,你可以用時間戳來替代上面說的immutable referential check,即先讀入文件時間戳,寫入前檢查時間戳是否發生變化,這么做能大大減少race的幾率,但不是解決了問題,因為讀入時間戳本身和寫入文件操作沒有原子性,可以出現race。
那么這種時候封裝原子操作是必要的,傳統的early lock也必要,但這是最細粒度鎖,它屬于原子操作鎖而不是事務鎖。
事務鎖本質上是big lock,即使要提高效率也只是每步操作檢查input,沒有邏輯難度,只有代碼量。
文件系統io是有需要寫原子操作鎖的情況的,數據庫和api操作應該由提供者保證rmw操作(read-modify-write),如果需要的話。
Big Picture所以問題不是簡單的fiber/coroutine vs async/await之爭,而是要站在更大的problem domain去全局的看。程序員需要的是全局的和一致的解決方案。
在前面的討論上說過了,fiber/coroutine完全是關于調度控制流程的,而callback/promise/async/await完全是關于結構化io操作的;兩者沒在同一個角度上談問題。
fiber/coroutine不是完整的問題答案,除非你的problem domain里最重要的問題是如何并發計算任務,io無所謂;
async/await回答了如何結構化io操作的問題,結合fp/immutable回答了如何在維護state和更新外部世界時解決事務性競爭問題。它是一個一攬子解決辦法,而且不難。
在針對state維護的問題上,state machine/event/state model是合格的,但是它與重io操作時的結構化io操作尤其是transactional更新問題沒有直接答案。nodejs本身不是general purpose的系統級開發語言和環境,它是domain specific language (dsl)。
我們不能說coroutine或者csp/channel在JavaScript上完全沒有意義,但是nodejs在io并發上已經做得很好,而如果還要在計算任務并發上做得很好,支持多核,目前看差距太大了,需要解決的問題很多很多。
未來JavaScript的未來肯定不在于目前worker引入的鎖,這是個joke,屬于monkey-patching。
在系統語言里不得不用的細粒度鎖也不該在JavaScript里出現,也不該用于解決事務問題。
Opportunistic Lock是被數據庫領域證實的和被廣泛接受的solution,只是在語言一級去實現primitive支持上有困難。它需要:
JS語言和JSVM真正支持immutable數據類型;
在JSVM里有Software Transactional Memory的實現;
理論上STM支持多核是沒問題的,系統語言的STM庫有很多成熟的,但是JS的語言對象模型是list/hash table,在JIT層面上又要編譯成類型對象,所以把對象模型扣在內存模型上并不簡單。
Final你應該花上幾周的時間了解一下Haskell。
Haskell是靜態語言,最純粹的simple typed lambda實現;它有著匪夷所思的強大的代數類型系統,但是到底是靜態的代數類型系統是未來,還是JIT的動態類型系統是未來,只有時間能回答了。
它有個搞笑的do語法,async/await該做的是就是haskell里do該做的。do/io monad也是最能說明白nodejs callback的設計初衷和最恰當的應用場景的。
在pure function, state monad, 和io monad之間劃分清楚的界限,是程序建模的巨大進步,而不是把io封裝在OO對象的操作里,它等于沒有區分state和io的不同。
無論用任何語言編程,這個建模方式和劃分模塊的辦法都是極具借鑒意義的;除非你的程序真的和老式程序一樣只需要封裝簡單的幾個文件操作。
時代不同了,web和network改變了我們編程的問題域,相應的我們在解法域需要新思維也就理所應當。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/83238.html
摘要:匿名函數是我們喜歡的一個重要原因,也是,它們分別消除了很多代碼細節上需要命名變量名或函數名的需要。這個匿名函數內,有更多的操作,根據的結果針對目錄和文件做了不同處理,而且有遞歸。 能和微博上的 @響馬 (fibjs作者)掰扯這個問題是我的榮幸。 事情緣起于知乎上的一個熱貼,諸神都發表了意見: https://www.zhihu.com/questio... 這一篇不是要說明白什么是as...
摘要:我們已經回答了的構造函數和原型都是誰的問題,現在牽扯出來一個,我們繼續檢查的構造函數是全局對象上屬性叫的對象的原型是個匿名函數,按照關于構造函數的約定,它應該是構造函數的屬性我們給這個對象起個名字,叫。 我不確定JavaScript語言是否應該被稱為Object-Oriented,因為Object Oriented是一組語言特性、編程模式、和設計與工程方法的籠統稱謂,沒有一個詳盡和大家...
摘要:一般這種情況會在類的構造函數內創建一個屬性,引用或詞法域的,但后面會看到我們有更好的辦法,避免這種手工代碼。 換句話說,StateUp模式把面向對象的設計方法應用到了狀態對象的管理上,在遵循React的組件化機制和基于props實現組件通訊方式的前提之下做到了這一點。 ---- 少婦白潔 閱讀本文之前,請確定你讀過React的官方文檔中關于Lifting State Up的論述: ht...
摘要:目的是為了解決在重用的時候,持久和方法重用的問題。換句話說你不用擔心把組件寫成模式不好重用,如果你需要傳統的方式使用,一下即可。 這篇文章所述的思想最終進化成了一個簡單的狀態管理模式,稱React StateUp Pattern,詳細介紹請參閱:https://segmentfault.com/a/11... 寫了一個非常簡單的實驗性Pattern,暫且稱為PurifiedCompon...
摘要:本文用于闡述模式的算法和數學背景,以及解釋了它為什么是里最完美的狀態管理實現。歡迎大家討論和發表意見。 本文用于闡述StateUp模式的算法和數學背景,以及解釋了它為什么是React里最完美的狀態管理實現。 關于StateUp模式請參閱:https://segmentfault.com/a/11... P-State, V-State 如果要做組件的態封裝,從組件內部看,存在兩種不同的...
閱讀 713·2023-04-25 19:43
閱讀 3910·2021-11-30 14:52
閱讀 3784·2021-11-30 14:52
閱讀 3852·2021-11-29 11:00
閱讀 3783·2021-11-29 11:00
閱讀 3869·2021-11-29 11:00
閱讀 3558·2021-11-29 11:00
閱讀 6105·2021-11-29 11:00