V8 中更快的異步函數和 promises


原文作者:Maya Lekova and Benedikt Meurer

譯者:UC 國際研發 Jothy


寫在最前:歡迎你來到“UC國際技術”公眾號,我們將為大家提供與客戶端、服務端、算法、測試、數據、前端等相關的高質量技術文章,不限于原創與翻譯。


一直以來,JavaScript 的異步處理都因其速度不夠快而名聲在外。 更糟糕的是,調試實時 JavaScript 應用 - 特別是 Node.js 服務器 - 并非易事,特別是在涉及異步編程時。 幸好,這些正在發生改變。 本文探討了我們如何在 V8(某種程度上也包括其他 JavaScript 引擎)中優化異步函數和 promise,并描述了我們如何提升異步代碼的調試體驗。

注意:如果你喜歡邊看演講邊看文章,請欣賞下面的視頻!如果不是,請跳過視頻并繼續閱讀。

視頻地址:

https://www.youtube.com/watch?v=DFP5DKDQfOc



一種新的異步編程方法


>> 從回調(callback)到 promise 再到異步函數 <<

在 JavaScript 還沒實現 promise 之前,要解決異步的問題通常都得基于回調,尤其是在 Node.js 中。 舉個例子?:

我們通常把這種使用深度嵌套回調的模式稱為“回調地獄”,因為這種代碼不易讀取且難以維護。

所幸,現在 promise 已成為 JavaScript 的一部分,我們可以以一種更優雅和可維護的方式實現代碼:

最近,JavaScript 還增加了對異步函數的支持。 我們現在可以用近似同步代碼的方式實現上述異步代碼:

使用異步函數后,雖然代碼的執行仍然是異步的,但代碼變得更加簡潔,并且更易實現控制和數據流。(請注意,JavaScript 仍在單線程中執行,也就是說異步方法本身并沒有創建物理線程。)


>> 從事件監聽回調到異步迭代 <<

另一個在 Node.js 中特別常見的異步范式是 ReadableStreams。 請看例子:

這段代碼有點難理解:傳入的數據只能在回調代碼塊中處理,并且流 end 的信號也在回調內觸發。 如果你沒有意識到函數會立即終止,且得等到回調被觸發才會進行實際處理,就很容易在這里寫出 bug。


幸好,ES2018 的一項新的炫酷 feature——異步迭代,可以簡化此代碼:


我們不再將處理實際請求的邏輯放入兩個不同的回調 - 'data' 和 ' end ' 回調中,相反,我們現在可以將所有內容放入單個異步函數中,并使用新的 for await...of 循環實現異步迭代了。 我們還添加了 try-catch 代碼塊以避免 unhandledRejection 問題[1]


你現在已經可以正式使用這些新功能了! Node.js 8(V8 v6.2/Chrome 62)及以上版本已完全支持異步方法,而 Node.js 10(V8 v6.8/Chrome 68)及以上版本已完全支持異步迭代器(iterator)和生成器(generator)!



異步性能提升

我們已經在 V8 v5.5(Chrome 55 和 Node.js 7)和 V8 v6.8(Chrome 68 和 Node.js 10)之間的版本顯著提升了異步代碼的性能。開發者可安全地使用新的編程范例,無需擔心速度問題。


上圖顯示了 doxbee 的基準測試,它測量了大量使用 promise 代碼的性能。 注意圖表展示的是執行時間,意味著值越低越好。

并行基準測試的結果,特別強調了 Promise.all() 的性能,更令人興奮:

我們將 Promise.all 的性能提高了 8 倍!

但是,上述基準測試是合成微基準測試。 V8 團隊對該優化如何影響真實用戶代碼的實際性能更感興趣。

上面的圖表顯示了一些流行的 HTTP 中間件框架的性能,這些框架大量使用了 promises 和異步函數。 注意此圖表顯示的是每秒請求數,因此與之前的圖表不同,數值越高越好。 這些框架的性能在 Node.js 7(V8 v5.5)和 Node.js 10(V8 v6.8)之間的版本得到了顯著提升。


這些性能改進產出了三項關鍵成就:

  • TurboFan,新的優化編譯器 ?

  • Orinoco,新的垃圾回收器 ?

  • 一個導致 await 跳過 microticks 的 Node.js 8 bug ?


在 Node.js 8 中啟用 TurboFan 后,我們的性能得到了全面提升。

我們一直在研究一款名為 Orinoco 的新垃圾回收器,它可以從主線程中剝離出垃圾回收工作,從而顯著改善請求處理。

最后亦不得不提的是,Node.js 8 中有一個簡單的錯誤導致 await 在某些情況下跳過了 microticks,從而產生了更好的性能。 該錯誤始于無意的違背規范,但卻給了我們優化的點子。 讓我們從解釋該 bug 開始:

上面的程序創建了一個 fulfilled 的 promise p,并 await 其結果,但也給它綁了兩個 handler。 你希望 console.log 調用以哪種順序執行呢?


由于 p 已經 fulfilled,你可能希望它先打印 'after: await' 然后打 'tick'。 實際上,Node.js 8 會這樣執行:


在Node.js 8 中 await bug


雖然這種行為看起來很直觀,但按照規范的規定,它并不正確。 Node.js 10 實現了正確的行為,即先執行鏈式處理程序,然后繼續執行異步函數。

Node.js 10 沒有 await bug

這種“正確的行為”可以說并不是很明顯,也挺令 JavaScript 開發者大吃一驚 ?,所以我們得解釋解釋。 在我們深入 promise 和異步函數的奇妙世界之前,我們先了解一些基礎。



>> Task VS Microtask <<

JavaScript 中有 task 和 microtask 的概念。 Task 處理 I/O 和計時器等事件,一次執行一個。 Microtask 為 async/await 和 promise 實現延遲執行,并在每個任務結束時執行。 總是等到 microtasks 隊列被清空,事件循環執行才會返回。


task 和 microtask 的區別


詳情請查看 Jake Archibald 對瀏覽器中 task,microtask,queue 和 schedule 的解釋。 Node.js 中的任務模型與之非常相似。


文章地址:

https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/


>> 異步函數<<

MDN 對異步函數的解釋是,一個使用隱式 promise 進行異步操作并返回其結果的函數。 異步函數旨在使異步代碼看起來像同步代碼,為開發者降低異步處理的復雜性。


最簡單的異步函數如下所示:

當被調用時,它返回一個 promise,你可以像調用別的 promise 那樣獲得它的值。

只有在下次運行 microtask 時才能獲得此 promise 的值。 換句話說,以上程序語義上等同于使用 Promise.resolve 獲取 value:

異步函數的真正威力來自 await 表達式,它使函數執行暫停,直到 promise 完成之后,再恢復函數執行。 await 的值是 promise fulfilled(完成)的結果。 這個示例可以很好地解釋:

fetchStatus 在 await 處暫停,在 fetch promise 完成時恢復。 這或多或少等同于將 handler 鏈接到 fetch 返回的 promise。

該 handler 包含 async 函數中 await 之后的代碼。


一般來說你會 await 一個 Promise,但其實你可以 await 任意的 JavaScript 值。 就算 await 之后的表達式不是 promise,它也會被轉換為 promise。 這意味著只要你想,你也可以 await 42:

更有趣的是,await 適用于任何 “thenable”,即任何帶有 then 方法的對象,即使它不是真正的 promise。 因此,你可以用它做一些有趣的事情,例如測量實際睡眠時間的異步睡眠:

讓我們按照規范看看 V8 引擎對 await 做了什么。 這是一個簡單的異步函數 foo:

當 foo 被調用時,它將參數 v 包裝到一個 promise 中,并暫停異步函數的執行,直到該 promise 完成。完成之后,函數的執行將恢復,w 將被賦予 promise 完成時的值。 然后異步函數返回此值。


>> V8 如何處理 await <<

首先,V8 將該函數標記為可恢復,這意味著該操作可以暫停并稍后恢復(await 時)。 然后它創建一個叫 implicit_promise 的東西,這是在調用異步函數時返回的 promise,并最終 resolve 為 async 函數的返回值。

簡單的異步函數以及引擎解析結果對比


有趣的地方在于:實際的 await。首先,傳遞給 await 的值會被封裝到 promise 中。然后,在 promise 后帶上 handler 處理函數(以便在 promise 完成后恢復異步函數),而異步函數的執行會被掛起,將 implicit_promise 返回給調用者。一旦 promise 完成,其生成的值 w 會返回給異步函數,異步函數恢復執行,w 也即是 implicit_promise 的完成(resolved)結果。


簡而言之,await v 的初始步驟是:

1. 封裝 v - 傳遞給 await 的值 - 轉換為 promise。

2. 將處理程序附加到 promise 上,以便稍后恢復異步函數。

3. 掛起異步函數并將 implicit_promise 返回給調用者。


讓我們一步步來完成操作。假設正在 await 的已經是一個已完成且會返回 42 的 promise。然后引擎創建了一個新的 promise 并完成了 await 操作。這確實推遲了這些 promise 下一輪的鏈接,正如 PromiseResolveThenableJob 規范表述的那樣。


然后引擎創造了另一個叫 throwaway(一次性)的 promise。 之所以被稱為一次性,是因為它不會由任何鏈式綁定 - 它完全存在引擎內部。 然后 throwaway 會被鏈接到 promise 上,使用適當的處理程序來恢復異步函數。 這個 performPromiseThen 操作是 Promise.prototype.then() 隱式執行的。 最后,異步函數的執行會暫停,并將控制權返回給調用者。


調用程序會繼續執行,直到調用棧為空。 然后 JavaScript 引擎開始運行 microtask:它會先運行之前的 PromiseResolveThenableJob,生成新的 PromiseReactionJob 以將 promise 鏈接到傳遞給 await 的值。 然后,引擎返回處理 microtask 隊列,因為在繼續主事件循環之前必須清空 microtask 隊列。


接下來是 PromiseReactionJob,它用我們 await 的 promise 返回的值 - 此時是 42 - 完成了 promise,并將該反應處理到 throwaway 上。 然后引擎再次返回 microtask 循環,循環中是最終待處理的 microtask。



接著,第二個 PromiseReactionJob 將結果傳遞回 throwaway promise,并恢復暫停執行的異步函數,從 await 返回值 42。


await 的開銷

總結以上所學,對于每個 await,引擎都必須創建兩個額外的 promise(即使右邊的表達式已經是 promise)并且它需要至少三個 microtask 隊列執行。 誰知道一個簡單的 await 表達式會引起這么多的開銷呢?!

我們來看看這些開銷來自哪里。 第一行負責封裝 promise。 第二行立即用 await 得到的值 v 解開了封裝。這兩行帶來了一個額外的 promise,同時也帶來了三個 microticks 中的兩個。 在 v 已經是一個 promise 的情況下(這是常見的情況,因為通常 await 的都是 promise),這中操作十分昂貴。 在不太常見的情況下,開發者 await 例如 42 的值,引擎仍然需要將它包裝成一個 promise。

事實證明,規范中已經有 promiseResolve 操作,只在必要時執行封裝:

此操作一樣會返回 promises,并且只在必要時將其他值包裝到 promises 中。 通過這種方式,你可以少用一個額外的 promise,以及 microtask 隊列上的兩個 tick,因為一般來說傳遞給 await 的值會是 promise。 這種新行為目前可以使用 V8 的 --harmony-await-optimization 標志實現(從 V8 v7.1 開始)。 我們也向 ECMAScript 規范提交了此變更,該補丁會在我們確認它與 Web 兼容之后馬上打上。


以下展示了新改進的 await 是如何一步步工作的:


讓我們再次假設我們 await 一個返回 42 的 promise。感謝神奇的 promiseResolve,現在 promise 只引用同一個 promise v,所以這一步中沒有任何關系。 之后引擎繼續像以前一樣,創建 throwaway promise,生成 PromiseReactionJob 在 microtask 隊列的下一個 tick 上恢復異步函數,暫停函數的執行,然后返回給調用者。


最終當所有 JavaScript 執行完成時,引擎開始運行 microtask,所以 PromiseReactionJob 被執行。 這個工作將 promise 的結果傳播給 throwaway,并恢復 async 函數的執行,從 await 中產生 42。


Summary of the reduction in await overhead


如果傳遞給 await 的值已經是一個 promise,那么這種優化避免了創建 promise 封裝器的需要,這時,我們把最少三個的 microticks 減少到了一個。 這種行為類似于 Node.js 8 的做法,不過現在它不再是 bug 了 - 它是一個正在標準化的優化!


盡管引擎完全內置,但它必須在內部創造 throwaway promise 仍然是錯誤的。 事實證明,throwaway promise 只是為了滿足規范中內部 performPromiseThen 操作的 API 約束。



最近的 ECMAScript 規范解決了這個問題。 引擎不再需要創建 await 的 throwaway promise - 大部分情況下[2]

Comparison of await code before and after the optimizations


將 Node.js 10 中的 await 與可能在 Node.js 12 中得到優化的 await 對比,對性能的影響大致如下:

async/await 優于手寫的 promise 代碼。 這里的關鍵點是我們通過修補規范[3]顯著減少了異步函數的開銷 - 不僅在 V8 中,而且在所有 JavaScript 引擎中。



開發體驗提升


除了性能之外,JavaScript 開發人員還關心診斷和修復問題的能力,這在處理異步代碼時并沒那么簡單。 Chrome DevTool 支持異步堆棧跟蹤,該堆棧跟蹤不僅包括當前同步的部分,還包括異步部分:

這在本地開發過程中非常有用。 但是,一旦部署了應用,這種方法就無法起作用了。 在事后調試期間,你只能在日志文件中看到 Error#stack 輸出,而看不到任何有關異步部分的信息。


我們最近一直在研究零成本的異步堆棧跟蹤,它使用異步函數調用豐富了 Error#stack 屬性。 “零成本”聽起來很振奮人心是吧? 當 Chrome DevTools 功能帶來重大開銷時,它如何才能實現零成本? 舉個例子?,其中 foo 異步調用了 bar ,而 bar 在 await promise 后拋出了異常:

在 Node.js 8 或 Node.js 10 中運行此代碼會輸出:

請注意,雖然對 foo() 的調用會導致錯誤,但 foo 并不是堆棧跟蹤的一部分。 這讓 JavaScript 開發者執行事后調試變得棘手,無論你的代碼是部署在 Web 應用程序中還是云容器內部。

有趣的是,當 bar 完成時,引擎知道它該繼續的位置:就在函數 foo 中的 await 之后。 巧的是,這也是函數 foo 被暫停的地方。 引擎可以使用此信息來重建異步堆棧跟蹤的部分,即 await 點。 有了這個變更,輸出變為:

在堆棧跟蹤中,最頂層的函數首先出現,然后是同步堆棧跟蹤的其余部分,然后是函數 foo 中對 bar 的異步調用。此變更在新的 --async-stack-traces 標志后面的 V8 中實現。


但是,如果將其與上面 Chrome DevTools 中的異步堆棧跟蹤進行比較,你會注意到堆棧跟蹤的異步部分中缺少 foo 的實際調用點。如前所述,這種方法利用了以下原理:await 恢復和暫停位置是相同的 - 但對于常規的 Promise#then() 或 Promise#catch()調用,情況并非如此。更多背景信息請參閱 Mathias Bynens 關于為什么 await 能打敗 Promise#then() 的解釋。



結論

感謝以下兩個重要的優化,使我們的異步函數更快了:

  • 刪除兩個額外的 microticks;

  • 取消 throwaway promise;


最重要的是,我們通過零成本的異步堆棧跟蹤改進了開發體驗,這些跟蹤在異步函數的 await 和 Promise.all() 中運行。

我們還為 JavaScript 開發人員提供了一些很好的性能建議:

  • 多用異步函數和 await 來替代手寫的 promise;

  • 堅持使用 JavaScript 引擎提供的原生 promise 實現,避免 await 使用兩個 microticks;


英文原文:https://v8.dev/blog/fast-async


好文推薦:

React 16.x 路線圖公布,包括服務器渲染的 Suspense 組件及Hooks等


“UC國際技術”致力于與你共享高質量的技術文章

歡迎關注我們的公眾號、將文章分享給你的好友


本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/450136.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/450136.shtml
英文地址,請注明出處:http://en.pswp.cn/news/450136.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Android應用開發—淺談MVX模式

MVX模式的文章太多了&#xff0c;這里不會再重述&#xff0c;關鍵我還不太懂&#xff0c;本文會從自己的一些思考&#xff0c;分析下MVX的分離思想&#xff0c;先占個坑&#xff01; Android應用架構這篇文章最后講到&#xff1a; Activity和Fragment變得非常輕量。他們唯一的…

音視頻互動平臺--P2P通信技術

壓縮包可以從這里獲得&#xff1a;http://anychat.storage.aliyun.com/AnyChatCoreSDK_Win32(MBCS)_V4.5.zip 下載后&#xff0c;首先將其解壓到一個臨時文件夾中&#xff0c;然后開始后續工作。 編譯后我們獲得的是.lib文件。 AnyChat SDK向上層應用開放了內核的P2P連接策略…

pigz 壓縮

壓縮工具--pigz 壓縮&#xff1a; tar cvf - 目錄名 | pigz -9 -p 24 > file.tgz pigz&#xff1a;用法-9是壓縮比率比較大&#xff0c;-p是指定cpu的核數。 解壓: pigz -d file.tgz 這時候是tar包&#xff0c;那么在用 tar -xvf file.tar 解包。 速度比較gz.tar格式的壓縮…

exgcd模板

逆元模板P1082 1 #include <cstdio>2 #include <algorithm>3 4 int exgcd(int a, int b, int &x, int &y) {5 if(!b) {6 x 1;7 y 0;8 return a;9 } 10 int g exgcd(b, a % b, x, y); 11 std::swap(x, y); 12 …

有關eclipse for java ee版本遇到的坑( Context initialization failed)

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 這幾天把以前網上看的視頻的源代碼拷貝到eclipse下面進行學習&#xff0c;當時用的是eclipse-jee-neon-M4a-win32-x86_64這個版本的ecli…

Android應用開發—TabLayout定制化Tab樣式

TabLayout的使用詳解基于此文&#xff0c;了解下setCustomView(childView)的使用。 TabLayout去掉指示器效果&#xff1a; app:tabIndicatorHeight"0dp"

即時通訊開發(逐行、隔行掃描)

早期的電視制式均采用隔行掃描&#xff0c;但是現在很多的高清、專業級的視頻采集卡都是采用逐行掃描模式&#xff0c;雖然現在的視頻設備和數字視頻技術已近有了很大的發展和進步&#xff0c;但是在時候中這兩種掃盲模式和顯示模式一直還存在&#xff0c;在前面我們介紹第四代…

ASP.NET Core 2.2 : 十六.扒一扒2.2版更新的新路由方案

ASP.NET Core 2.2 : 十六.扒一扒2.2版更新的新路由方案 原文:ASP.NET Core 2.2 : 十六.扒一扒2.2版更新的新路由方案ASP.NET Core 從2.2版本開始&#xff0c;采用了一個新的名為Endpoint的路由方案&#xff0c;與原來的方案在使用上差別不大&#xff0c;但從內部運行方式上來說…

ES6學習筆記

ES6學習筆記 在學習ES6的過程中做的一些記錄&#xff0c;用于未來的快速回憶。let&const 作用域的概念 ES6新增塊級作用域的概念&#xff08;一個大括號括起來的部分就是一個塊作用域&#xff09;let與const用于在塊級作用域中聲明變量&#xff0c;該變量僅在當前塊級作用域…

用jenkins創建節點

原料&#xff1a;(1)jre下載鏈接&#xff1a;https://www.oracle.com/technetwork/java/javase/downloads/jre8-downloads-2133155.html (2)jdk:下載鏈接&#xff1a;https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html 一、創建SLAVE節點…

統計git倉庫一些commit數據

基于git統計某個人的代碼提交行數 git log --author"xxx" --prettytformat: --since1.hour.ago --numstat | awk { add $1 ; subs $2 ; loc $1 - $2 } END { printf "added lines: %s removed lines : %s total lines: %s\n",add,subs,loc } - 統計倉…

JAXB: XML綁定的Java體系結構

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 用于XML綁定的Java體系結構&#xff08;JAXB&#xff09;是一種軟件框架&#xff0c;它允許Java開發人員將Java 類映射到XML表示。JAXB提…

解決 Script Error 的另類思路

2019獨角獸企業重金招聘Python工程師標準>>> 本文由小芭樂發表 前端的同學如果用 window.onerror 事件做過監控&#xff0c;應該知道&#xff0c;跨域的腳本會給出 "Script Error." 提示&#xff0c;拿不到具體的錯誤信息和堆棧信息。 這里讀者可以跟我一…

大平臺的局限

這篇文章算是二稿。初稿使的是慣用的賣弄筆法&#xff0c;寫到盡興時去查了查資料&#xff0c;哦草&#xff0c;錯了好多。悶悶不樂。后來就不敢再鬼扯&#xff0c;老老實實干巴巴地講觀點。 做產品的人都喜歡大平臺&#xff0c;好像男人都喜歡大胸脯女郎&#xff0c;但是胸脯大…

Lisenter筆記

EventListener與EventObject要完成在線用戶列表的監聽器&#xff0c;需要使用如下幾個接口&#xff1a;ServletContextListener接口&#xff1a;在上下文初始化時設置一個空的集合到application之中&#xff1b;HttpSessionAttributeListener接口&#xff1a;用戶增加session屬…

Android應用開發—重載fragment構造函數導致的lint errors

背景&#xff1a;在一次release打包中發現lint報以下錯誤&#xff1a; Error: Avoid non-default constructors in fragments: use a default constructor plus Fragment#setArguments(Bundle) instead [ValidFragment] 根據后面的log提示是由于重載了fragment的構造函數&…

迅雷影音怎樣 1.5倍速度播放

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 看視頻 覺得播放速度太慢&#xff0c;想讓1.5速度播放可以這樣設置&#xff1a; 點擊快進按鈕&#xff0c;點一次變為1.1倍&#xff0c…

【Java】Mybatis mapper動態代理方式

前言 我們在使用Mybatis的時候&#xff0c;獲取需要執行的SQL語句的時候&#xff0c;都是通過調用xml文件來獲取&#xff0c;例如&#xff1a;User user (User) sqlSession.selectOne("cn.ddnd.www.Entity.User.getUser", "xue8qq.com");。這種方式是通過…

git pull時沖突的幾種解決方式

僅結合本人使用場景&#xff0c;方法可能不是最優的 1. 忽略本地修改&#xff0c;強制拉取遠程到本地 主要是項目中的文檔目錄&#xff0c;看的時候可能多了些標注&#xff0c;現在遠程文檔更新&#xff0c;本地的版本已無用&#xff0c;可以強拉 git fetch --allgit reset --h…

Android應用開發—eventBus發布事件和事件處理的時序關系

占坑&#xff0c;簡單說明下eventBus發布事件和事件處理的時序關系。 什么時候使用sticky&#xff1a; 當你希望你的事件不被馬上處理的時候&#xff0c;舉個栗子&#xff0c;比如說&#xff0c;在一個詳情頁點贊之后&#xff0c;產生一個VoteEvent&#xff0c;VoteEvent并不立…