<div>
所以這篇文章提到的內容,我覺得有必要來寫一下 async/await 的歷史。
補充說明一下,原文章在談到 JavaScript 的 async/await 歷史時,我認為並沒有錯。但async/await 不是 JavaScript 創造的東西。
原文章討論的是「在 JavaScript 中的演變」,所以在那個語境下是正確的。
然而,當你知道 async/await 是從哪裡來的時候,就會發現這個語法是多麼多天才們接力賽的結果,這真的蠻有趣的。
因此,這篇文章將探討 JavaScript 的 async/await 是從何而來,追溯它的根源。
async/await 的父親是 C我想大多數人都知道,JavaScript 的 async/await 幾乎直接採用了 C# 的 async/await(2012年,C# 5.0) 語法。
// JavaScript (ES2017)
async function fetchData(url) {
const response = await fetch(url);
const data = await response.json();
return data;
}
// C# 5.0 (2012)
async Task<string> FetchDataAsync(string url) {
var response = await httpClient.GetAsync(url);
var content = await response.Content.ReadAsStringAsync();
return content;
}
那麼,C# 的 async/await 是從哪裡誕生的呢?
async/await 的父親是 FC# 的設計者是 Anders Hejlsberg。他受到了同樣在 Microsoft 內部開發的函數型語言 F# 的影響。
F# 的設計者 Don Syme 等人於 2007 年在 F# 中引入了 非同步工作流程(Asynchronous Workflows) 這個功能。
// F# 的非同步工作流程 (2007)
let fetchData url = async {
let! response = httpClient.AsyncDownload(url) // ← let! 非同步等待
let! parsed = parseAsync(response) // ← 當結果到來時繼續執行
return parsed
}
F# 的 let! 變成了 C# 的 await,而 async { ... } 區塊則成為了方法的 async 修飾詞。Hejlsberg 的團隊將這種 F# 的做法翻譯為命令式編程,並推出了 C# 5.0 的 async/await。
那麼,Syme 他們為什麼需要發明非同步工作流程呢?
要理解這一點,就需要知道當時伺服器端開發面臨的一個嚴重問題。
在 2000 年代,隨著 Web 應用程式的大型化,伺服器面臨的一個問題是:
C10K 問題(十萬個連接問題)。
這是「一台伺服器能否同時處理一萬個客戶端連接?」的挑戰。這一問題由 Dan Kegel 在 1999 年提出,並成為當時伺服器工程師面臨的迫切問題。
傳統的伺服器程式會將一個連接分配一個進程或線程。然而,進程或線程需要大量的系統資源。萬一要有一萬個連接則需要一萬個線程?那樣的話整個操作系統都會崩潰。
與此同時,CPU 的時脈頻率的提升也已經遇到了瓶頸(所謂的「不再免費的午餐」)。已經無法再宣稱「什麼都不做明年 CPU 就會變快」的時代已經來臨。
對於 C10K 問題的解決方案是 事件驅動編程。
不再是一個連接一個線程,而是用少數幾個線程來處理大量的連接,將 I/O(網路通訊或磁碟讀寫)的完成通知作為「事件」,並用回呼函數繼續處理。這樣的話無論是一萬還是十萬都能夠用少量的線程來應對。
但是,這種方式帶來了巨大的代價。
以事件驅動的方式來編寫非同步 I/O 就是要「將所有的處理都寫成回呼函數的鏈條」。
在當時的 .NET(C# 2.0 到 3.0 時期)裡面這樣做會變成這樣:
// 當時 C# 的非同步代碼(APM 模式)
// 只要執行三個非同步處理就變成這樣
void StartChain() {
var request = WebRequest.Create("https://api.example.com/data");
request.BeginGetResponse(new AsyncCallback(ResponseCallback), request);
}
void ResponseCallback(IAsyncResult result) {
try {
var request = (WebRequest)result.AsyncState;
var response = request.EndGetResponse(result);
var stream = response.GetResponseStream();
// 接下來的非同步處理的回呼又會在這裡嵌套……
} catch (Exception ex) {
// 錯誤處理也被分散到各個回呼
}
}
這就是回呼地獄。 對於 JavaScript 的人來說這是耳熟能詳的情景。
在同步代碼中可以輕易寫的 try-catch(異常處理)或 using(資源的確保釋放)在跨越回呼時會瞬間崩潰。代碼變得支離破碎,除錯困難重重,並且成為錯誤的溫床。
這正是 Don Syme 等人想要用非同步工作流程解決的問題。
時間稍微推進。
2009 年,Node.js 問世。
這是理念上在 JavaScript 上實現的「事件驅動的非同步 I/O 處理伺服器端運行時」。Ryan Dahl 創建它時正是著眼於 C10K 問題。
在這裡發生了一件有趣的事情。
JavaScript 本來就是瀏覽器的語言。在瀏覽器中,為了避免 UI 僵死,設計上就已經內建了以單線程非同步處理事件的機制。因此,瀏覽器的前端工程師們早已經為 回呼地獄所苦。這正是原文章所提到的「async/await 為何誕生 ~ 追溯非同步處理的歷史 ~」中所討論的內容。
隨著 Node.js 的登場,伺服器端也開始使用 JavaScript 來實現,前端和伺服器端這兩個回呼地獄在 JavaScript 這個共通語言上相遇了。
在這樣的情況下,已經在伺服器端的 C# 中取得高度評價的 async/await 自然也就被引入了 JavaScript。ES2015(ES6)標準化了 Promise,而在 ES2017 中引入了 async/await,這就是這一切的結果。
因為用回呼寫非同步處理實在很辛苦,所以 async/await 的誕生並不是錯的理解,但將其簡化可能過於簡單了一點。
在其背後,存在著 C10K 問題這一時代的需求,實際上這並不是來源於 JavaScript 本來的主要舞台——前端(UI)中。
好了。到目前為止是「async/await 到達 JavaScript 的過程」的故事。
接下來我們將向更小眾的方向發展。
再追溯 F# 的非同步工作流程的源流,這對於喜歡編程語言演化的人來說是一個有趣的話題。
do 語法(1990年代)F# 的 async { ... } 和 let! 的直接父親來自於純函數型語言 Haskell。
Haskell 為了將「副作用(I/O 或狀態變更)」與純函數隔離,引入了因為過於複雜而著名的 Monad 概念。
Haskell 也引入了 do 語法作為將 Monad 寫成「像一般命令式編程那樣從上到下」的語法糖(約在 1994 到 1996 年之間)。
-- Haskell 的 do 語法
main = do
content <- readFile "hello.txt" -- ← 讀取檔案(I/O)
let upper = map toUpper content -- ← 純粹的轉換
putStrLn upper -- ← 輸出至畫面(I/O)
請注意這個 <- 符號。它的意義是「取出 I/O 操作的結果並綁定到變數」。
這個 <- 就是 F# 的 let!、C# 的 await,以及 JavaScript 的 await 的直接祖先。
而且這裡存在一個重要事實。Haskell 開發的核心人物之一 Simon Peyton Jones 是 Microsoft Research 的一員,把他影響到 Don Syme 是可以理解的。
Don Syme 將 Haskell 的 Monad 和 Computation Expressions(計算式)這一通用框架引入 F#,並實現了 async { ... } 作為具體的應用實例,這是理論轉化為實際應用的卓越工程。
yield另一個源流是 迭代器(生成器)。
「逐個返回元素」的機制可以追溯到 1970 年代的 CLU 和 Icon。這一概念後來成為了一個巨大的伏筆。
在 2005 年,C# 2.0 中引入了 yield return。這是一種「返回一個值並暫停處理,當下次被調用時從暫停的地方繼續」的功能。
// C# 2.0 的 yield return
IEnumerable<int> GetNumbers() {
yield return 1; // ← 在這裡暫停,返回 1 給調用者
yield return 2; // ← 下次被調用時從這裡繼續返回 2
yield return 3;
}
換句話說,「在函數的中途暫停處理、以後再繼續」的技術在 C# 2.0 時期已經實現於編譯器中。
F# 的非同步工作流程之所以具有革命性,正是因為它意識到這個 yield 的機制不僅可以用來「返回清單的元素」,還可以用來 「返回未完成的非同步處理(等待 I/O)」。
JavaScript 也可以使用 function* 和 yield 來使用生成器,同樣是這一系譜的一部分。
如果再向歷史追溯,就會到達 協程(Coroutine)。
可以在中途暫停執行,並在稍後從那裡重新開始的函數。
這一概念由 Melvin Conway 在 1958 年提出。沒錯,就是那位以「康威法則」聞名的 Conway。在 1967 年,面向對象的始祖 Simula 67 將協程作為語言功能納入。
async/await 在背後所做的事情——在不阻塞線程的情況下暫停處理,並在 I/O 等待結束後從那裡重新開始——本質上就是這個協程本身。
這一概念在 1958 年出現,經過近 60 年的時間,形變為 async/await 的形式,現已在 C#、JavaScript、Python、Rust 等很多語言中成為理所當然的使用方式。
| 年代 | 誰做了什麼 | 對後世的貢獻 |
|---|---|---|
| 1958 | Melvin Conway 提出協程 | 「中斷和再開」的概念 |
| 1967 | 協程在 Simula 67 中實現 | 語言功能的實績 |
| 1970年代 | CLU, Icon 中出現 yield 相當的概念 |
迭代器的原型 |
| 1990年代 | Haskell 確立了 Monad + do 語法 |
「優雅書寫副作用」的語法模式 |
| 1999 | Dan Kegel 提出了 C10K 問題 | 非同步 I/O 的需求顯現 |
| 2005 | C# 2.0 的 yield return |
編譯器對於函數中斷的支持 |
| 2007 | F# 的非同步工作流程 | 統合一切 |
| 2009 | Node.js 的登場 | 在 JS 中實現伺服器端非同步 |
| 2012 | C# 5.0 的 async/await |
進入主流語言 |
| 2015 | ES2015 的 Promise 標準化 | JS 中非同步的基礎 |
| 2017 | ES2017 的 async/await |
60 年累積的成就 |
程式語言的演進就是過去想法的重新發現和再建構的連續體。
我們隨意使用的 await 這個詞彙中,凝聚了創造協程的 Conway、讓 Monad 實用化的 Haskell 社群、在 Microsoft Research 中將理論和實用橋接的 Simon Peyton Jones 和 Don Syme、以及將它帶入主流的 Anders Hejlsberg……許多天才的努力。
下次當你寫 await 的時候,希望你能稍微想起 60 年的歷史就在這裡面。
async/await 的影響。原文出處:https://qiita.com/Maki-Daisuke/items/1da25e18c1bcb68880a3