🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

<div>
所以這篇文章提到的內容,我覺得有必要來寫一下 async/await 的歷史。

補充說明一下,原文章在談到 JavaScript 的 async/await 歷史時,我認為並沒有錯。但async/await 不是 JavaScript 創造的東西。

原文章討論的是「在 JavaScript 中的演變」,所以在那個語境下是正確的。
然而,當你知道 async/await 是從哪裡來的時候,就會發現這個語法是多麼多天才們接力賽的結果,這真的蠻有趣的。

因此,這篇文章將探討 JavaScript 的 async/await 是從何而來,追溯它的根源。

JavaScript 的 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 是從哪裡誕生的呢?

C# 的 async/await 的父親是 F

C# 的設計者是 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 他們為什麼需要發明非同步工作流程呢?
要理解這一點,就需要知道當時伺服器端開發面臨的一個嚴重問題。

時代背景:C10K 問題與回呼地獄

10,000 連接的壁壘

在 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 等人想要用非同步工作流程解決的問題。

Node.js — 邂逅之地

時間稍微推進。

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# 的非同步工作流程的源流,這對於喜歡編程語言演化的人來說是一個有趣的話題。

源流一:Haskell 的 Monad 和 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 JonesMicrosoft 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,1958年)

如果再向歷史追溯,就會到達 協程(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 年的歷史就在這裡面。


參考文獻

  • Conway, M.E. (1963). "Design of a Separable Transition-Diagram Compiler". Communications of the ACM, 6(7), 396–408.
    • 首次正式論文化協程的概念。
  • Kegel, D. (1999). "The C10K Problem". http://www.kegel.com/c10k.html
    • 提出 C10K 問題。
  • Sutter, H. (2005). "The Free Lunch Is Over: A Fundamental Turn Toward Concurrency in Software". Dr. Dobb's Journal.
  • Syme, D., Petricek, T., Lomov, D. (2011). "The F# Asynchronous Programming Model". Proceedings of PADL 2011.
    • F# 的非同步工作流程的設計及其對 C# 的 async/await 的影響。
  • Peyton Jones, S. (2001). "Tackling the Awkward Squad: monadic input/output, concurrency, exceptions, and foreign-language calls in Haskell".
    • Haskell 中 Monad 和 do 語法對於 I/O 處理的解釋。
  • Hejlsberg, A. et al. (2012). "The C# Programming Language (Covering C# 5.0)", 第 4 版。
  • Shriram, A. (2012). "Async in 4.5: Worth the Await". Microsoft Dev Blogs.
    • .NET 4.5 中 async/await 的設計理念。
    • 其實早在這裡就已經提到,用 async/await 能優雅地處理 UI,前端與伺服器端的回呼地獄的相遇其實不是 JavaScript 首次發生的。
  1. Anders Hejlsberg 是 C# 的設計者,也是 Turbo Pascal 和 Delphi 的設計者,同時還是 TypeScript 的設計者。程序語言界的傳奇人物。
  2. Dan Kegel. "The C10K problem" (1999). http://www.kegel.com/c10k.html
  3. Herb Sutter 的著名文章 http://www.gotw.ca/publications/concurrency-ddj.htm "The Free Lunch Is Over"(2005 年)。講述了「隨著摩爾定律的推進,CPU 會變快,因此程序會自動變快的時代已經結束」。
  4. 對於 Monad 的解釋……這裡就不贅述了。「不要將其比作墨西哥捲餅」這一點只提醒大家。感興趣的人可以參考 很棒的 Haskell 快樂學習!
  5. 「系統的設計反映了設計它的組織的溝通結構」,就是這樣。原來你也曾經是協程的那個人。

原文出處:https://qiita.com/Maki-Daisuke/items/1da25e18c1bcb68880a3


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝20  
563
🥈
我愛JS
💬2  
7
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付