ということを書いた手前、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 だ 1。彼は同じくMicrosoft内で開発されていた関数型言語 F# から影響を受けている。
F#の設計者 Don Syme らは2007年に、Asynchronous Workflows(非同期ワークフロー) という機能をF#に導入した。
// 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問題(シーテンケー問題)だ。
これは「1台のサーバーで同時に1万クライアントの接続を処理できるか?」 という課題だ。1999年にDan Kegel氏が提起し 2、当時のサーバーエンジニアにとって切実な問題だった。
従来のサーバープログラムは、1つの接続に対して1つのプロセスやスレッドを割り当てていた。ところが、プロセスやスレッドはOSのリソースを大量に消費する。1万接続に1万スレッド? そんなことをしたらOSごと死ぬ。
同時に、CPUのクロック周波数の向上も頭打ちになっていた(いわゆる「フリーランチの終わり」3)。もう「何もしなくても来年にはCPUが速くなるから大丈夫」とは言えない時代が来ていた。
イベント駆動への転換
C10K問題に対する答えは、イベント駆動型プログラミングだった。
1接続1スレッドではなく、少数のスレッドで大量の接続を「イベント」として捌く。I/O(ネットワーク通信やディスク読み書き)の完了を「イベント」として通知してもらい、コールバック関数で処理を続行する。これなら1万だろうが10万だろうが、少ないスレッドで対応できる。
しかし、この方式には大きな代償があった。
コールバック地獄の襲来
イベント駆動型で非同期I/Oを書くということは、「処理のすべてをコールバック関数の連鎖として記述する」ということだ。
当時の.NET(C# 2.0〜3.0時代)でこれをやるとこうなる:
// 当時のC#の非同期コード(APMパターン)
// 3つの非同期処理を順番に実行するだけでこの有様
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 が登場する。
「イベント駆動で非同期I/Oを処理するサーバーサイドランタイム」という思想をJavaScriptの上に実装したものだ。Ryan Dahlが、まさにC10K問題を意識して作ったとされている。
ここで面白いことが起きた。
JavaScriptは元々ブラウザの言語だ。ブラウザでは、UIがフリーズしないようにシングルスレッドで非同期にイベントを捌く設計が最初から組み込まれていた。だから、ブラウザのフロントエンドエンジニアたちはすでに コールバック地獄に苦しんでいた。ここがまさに、元ネタ「async/awaitはなぜ生まれたのか ~ 非同期処理の歴史を辿る ~」で語られていたことだ。
そしてNode.jsの登場により、サーバーサイドもJavaScriptで実装するようになり、フロントエンドとサーバーサイド、2つのコールバック地獄がJavaScriptという共通言語の上で邂逅を果たしたのだ。
こうなると、サーバーサイドのC#ですでに高い評価を得ていた async/await をJavaScriptに取り入れるのは、ごく自然な流れだった。ES2015(ES6)で Promise が標準化され、ES2017で async/await が導入されたのがこの帰結だ。
非同期処理をコールバックで書くのが辛かったから async/await が生まれた。そう捉えるのは間違ってはいないが、それはあまりにも単純化しすぎていると思う。
その裏には、C10K問題という時代の要請があり、実はそれはJavaScriptのもともとの主戦場であったフロントエンド(UI)から生まれたものではなかったのである。
ここからは言語オタクの世界へ
さて。ここまでが「async/awaitがJavaScriptにたどり着くまで」の話だ。
ここからはもう少しマニアックな方向に進む。
F#の非同期ワークフローの、さらに源流を辿っていこう。プログラミング言語の進化が好きな人向けの話だ。
源流その1:Haskellのモナドと do 構文(1990年代)
F#の async { ... } と let! の直接の親は、純粋関数型言語 Haskell にある。
Haskellは「副作用(I/Oや状態変更など)」を純粋な関数から隔離するために、難しくてよくわからないと有名な モナド(Monad) をプログラミングに持ち込んだ 4。
そしてHaskellは、モナドを「普通の命令型プログラミングっぽく上から下へ書ける」ようにするためのシンタックスシュガーとして do 構文 を導入した(1994~96年頃)。
-- 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 に所属していた。同じMicrosoft Research内でF#を設計していたDon Symeに影響を与えたのは、考えてみれば当然のことだ。
Don Symeは、HaskellのモナドとComputation Expressions(計算式)という汎用的な枠組みをF#に導入し、その具体的な応用例の一つとして async { ... } を実装した。理論を実用に変えた、見事なエンジニアリングだった。
源流その2: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 でジェネレータが使えるけど、あれもまさに同じ系譜上にある。
究極の源流:コルーチン(1958年)
歴史をさらに遡ると、最後に行き着くのは コルーチン(Coroutine) だ。
途中で実行を一時停止して、あとでそこから再開できる関数。
この概念を1958年にMelvin Conwayが提唱した。そう、「コンウェイの法則」で有名なあのConwayだ 5。1967年にはオブジェクト指向の始祖 Simula 67 にコルーチンが言語機能として組み込まれた。
async/await が裏側でやっていること——スレッドをブロックせずに処理を中断し、I/O待ちが終わったらそこから再開する——は、本質的にはこのコルーチンそのものだ。
1958年に生まれた概念が、60年近い歳月を経て、形を変えて async/await という見た目になり、今やC#やJavaScriptだけでなく、PythonやRustなど多くの言語で当たり前のように使われているのはご存知のとおりだ。
まとめ:バトンリレーの全体像
| 年代 | 誰が何をしたか | 後世への貢献 |
|---|---|---|
| 1958 | Melvin Conwayがコルーチンを提唱 | 「中断と再開」の概念 |
| 1967 | Simula 67にコルーチンが実装される | 言語機能としての実績 |
| 1970s | CLU, Iconに yield 相当の概念が登場 |
イテレータの原型 |
| 1990s | Haskellがモナド + 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、モナドを実用化した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への影響。
- F#の非同期ワークフローの設計と、C#の
- Peyton Jones, S. (2001). "Tackling the Awkward Squad: monadic input/output, concurrency, exceptions, and foreign-language calls in Haskell".
- Haskellにおけるモナドとdo構文によるI/O処理の解説。
- Hejlsberg, A. et al. (2012). "The C# Programming Language (Covering C# 5.0)", 4th Edition.
- Shriram, A. (2012). "Async in 4.5: Worth the Await". Microsoft Dev Blogs.
- .NET 4.5のasync/awaitの設計思想。
- 実はここですでに、async/await でUIがきれいに書けるということが述べられている。フロントエンドとサーバーサイドのコールバック地獄が出会ったのは、JavaScriptが最初ではなかったりする。
-
Anders HejlsbergはC#の設計者であると同時に、Turbo PascalやDelphiの設計者でもあり、TypeScriptの設計者でもある。プログラミング言語界のレジェンド中のレジェンド。 ↩
-
Dan Kegel. "The C10K problem" (1999). http://www.kegel.com/c10k.html ↩
-
Herb Sutterの有名な記事 "The Free Lunch Is Over"(2005年)。「ムーアの法則でCPUが速くなるから、何もしなくてもプログラムが速くなる」という時代はもう終わった、という話。 ↩
-
モナドの説明は……ここでは省略する。「ブリトーに例えるな」とだけ言っておく。気になる人は すごいHaskellたのしく学ぼう! を読もう。 ↩
-
「システムの設計は、それを設計する組織のコミュニケーション構造を反映する」というアレ。コルーチンの人だったんですね。 ↩