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