能和微博上的 @響馬 (fibjs做者)掰扯這個問題是個人榮幸。javascript
事情緣起於知乎上的一個熱貼,諸神都發表了意見:java
https://www.zhihu.com/questio...node
這一篇不是要說明白什麼是async/await,而是闡述爲何會在編程技術這麼多年後出現和流行了這個東西,讀懂這篇文章你須要對async/await有很透徹的機制理解。程序員
若是是寫系統程序,流行的編程範式是面向對象,這很是成熟不用多說;但若是是寫微服務(restful api server),狀況不一樣。算法
寫微服務的時候數據不是從文件或數據庫讀取、去串行化、構造對象而後在內存中維護對象;而是向數據庫、cache、或者API提取數據,計算後儘快輸出結果;數據庫
前者的數據對象生命週期較長,object-oriented範式很合適,它研究一個對象的狀態機和如何響應外部事件;編程
後者的數據生命週期很短,並且更糟糕的,各類input數據的結構也不很穩定,常常變化,因此這個時候OO的模式就顯得笨重和低效了,在這個時候對data的處理不是object-oriented範式,而是transformation-oriented範式。api
後者致使了函數式編程的興起,這裏無法仔細討論函數式編程的方方面面,咱們僅僅說transformation的問題。promise
這種編程範式下一次api服務的生命週期在心理模型上一個函數的開始和結束,這個函數須要從不少地方pull數據,若是是從內存中直接pull,這個在fp裏叫作state monad;若是是異步pull數據,包括文件、數據庫、其餘api,這個叫io monad。性能優化
OO的本質站在fp的角度看是如何維護state monad,若是程序中有stateful的部分,或多或少都會有,用oo建模不是問題;訪問這些state都是同步的也不是問題;
async/await的出現是爲了解決第二個問題,io monad。
在採用transformation和fp方式寫微服務的時候,常見狀況不是處理單一數據單元,而是數據集合,集合數據的變換是map/filter,聚合是reduce(廣義);這個過程能夠有條件,能夠是nested,其結構取決於你的業務邏輯和solution model,不是編程技術解決的。
因此你大致能夠把這些邏輯先用同步的方式寫出來,假定全部異步得到的數據均可以同步得到,而後把須要pull的數據改爲用async/await去獲取;這在結構上很清晰;
在這個時候開發者考慮的問題不是如何對付單一數據的異步獲取問題,而是考慮這些異步過程之間如何去串行和併發的問題;換句話說,他們的執行序是你要program的邏輯的一部分。既然他們是programming邏輯的一部分,那麼他們顯示存在就理所應當。
這裏說的串行和併發僅指從io monad裏pull數據的操做,不是指程序中其餘部分的執行體之間的併發或並行。下同。
這裏有兩個平衡:
第一:若是要追求service time越短越好,也就是提升響應時間,那麼這些異步就會象project軟件裏的甘特圖同樣,能併發的儘早併發,service time取決於最長的路徑。一般瓶頸都是io不是算力,除非設計有問題或者算法寫得太爛。
這種優化極可能帶來代碼結構的不清晰,可是它是能夠作並且容易作的,在async/await模式下,由於它在代碼層面上基本上保留了這個甘特圖關係。
它適應業務變化的能力也很好,在業務邏輯變化必須修改的時候,開發者總有一個比較清除的甘特圖,若是你不在async子函數裏封裝太長的沒必要要邏輯的話;和OO建模時咱們反覆問一個對象是否是single responsibility同樣,一個async函數的封裝越原子化,越容易讓開發者在上層組合順序和併發。
這裏我不去批判thread或者fiber或者goroutine或者coroutine的模型,只強調異步數據的pull邏輯的原子化,這是高併發微服務編程對開發者提出來的新問題,原則上任何一種開發語言和開發模型均可以作到對等的性能和可用性,但實踐上大多數狀況下,程序員不把program異步pull數據的順序和併發當成是本身編程邏輯的一部分,去享受thread model下的編程邏輯簡單,這是不對的;你能夠有理由不急着去作service time優化,可是不意味着你根本不知道它的模型邏輯和若是要去優化,作法是什麼。
第二:async函數對gc的壓力很大,由於compiler很難去判斷在運行時哪些域內變量能夠回收,這不一樣於閉包變量,閉包變量的生命週期判斷在源碼級的詞法域就能夠分析出來;因此async函數的執行應該是短生命週期的。
貼一小段代碼,實際項目代碼,沒什麼特別的,Promise用了bluebird庫:
async storeDirAsync(dir) { let entries = await fs.readdirAsync(dir) let treeEntries = await Promise .map(entries, async entry => { let entryPath = path.join(dir, entry) let stat = await fs.lstatAync(entryPath) if (stat.isDirectory()) return ['tree', entry, await this.storeDirAsync(entryPath)] if (stat.isFile()) return ['blob', entry, await this.storeFileAsync(entryPath)] return null }) .filter(treeEntry => !!treeEntry) return await this.storeObject(treeEntries) }
這是一個class方法。
它的第一步是獲取了一個文件夾內的entries
,而後用Bluebird庫提供的map方法應用了一個async函數上去,這是個匿名函數。
匿名函數是咱們喜歡fp的一個重要緣由,functor chaining也是,它們分別消除了不少代碼細節上須要命名變量名或函數名的須要。
這個匿名函數內,有更多的await操做,根據fs.stat的結果針對目錄和文件作了不一樣處理,並且有遞歸。async以內是順序執行的,但async在map裏是併發的,這些東西都顯式擺在代碼層面上。
若是任務範圍更大,你能夠把不少promise聚合在儘量早的時候併發。
固然這個寫法沒有美好到能夠直接寫entries.mapAsync()
的程度,但基本上作到了上述的要求:在源碼層面上對順序和併發有一覽,有控制,容易變動。
說到底,async是讓這種順序和併發的書寫和維護變得容易,而不是說我不要寫併發,一切順序走;可是反過來講它的效率不是最好的,在node裏最好的效率目前和可見的將來都是裸寫callback,那是最後的性能優化了。
最後咱們說這個寫法的一個有點麻煩的坑。
在class方法裏寫async有個this binding的問題,搞出來一個閉包變量並非最好的辦法,Bluebird庫裏有Promise.bind方法解決這個問題,上述代碼中用arrow function的lexical scope bind this也是一個辦法(也是推薦的辦法)。
node.js是我寫過的最好的純粹event model模型的開發環境;遠好過天生thread模型倒回來打不少non-blocking補丁的作法;
javascript領域,和目前整個編程界,在使用asynchronous(異步)這個詞來講咱們在這篇文章裏聊的問題,這是個錯誤,asynchronous在編程上有其餘含義,不管是寫系統程序(signal handler)仍是寫內核或者裸金屬(isr);這個問題的準確表述是:non-blocking。
而對應non-blocking的solution模型是如何調度(schedule)執行體;再而後的問題轉換成你須要顯式調度仍是隱式調度?
若是你認爲:
service time是須要追求的
調度邏輯是常常隨着業務邏輯變化而變化的
完整的數據流變換邏輯和調度邏輯都應該在代碼層面上呈現總覽,是top-down的構建的
你應該選擇async/await;
反之,你但願編程極致簡單,調度不在你的solution模型以內,你bottom-up構建邏輯,應該遠離javascript,選擇thread模型。
「請把你的左手放在本身的大咪咪上,回答一個問題,調度執行體和調度io是一回事嗎?」
白潔搖搖頭。
「我也認爲不是,可是不少runtime library並無區分二者。」 said I.
JavaScript的event model並無所謂的調度執行體的設計,它本質上只有調度io。