ブラウン大学のPLグループのGavin Grayによる論文『A Design Space Exploration of Async/Await』は、ほとんどのプログラマーが落ち着いた機能だと考えているasync/awaitを体系的に調べている。その結果は、どこでも基本的に同じ semantics だと考えている人には衝撃的だ。バックグラウンドでログ書き込みを発火させ、1秒スリープ後に「C」と出力するプログラムは、ランタイムによって少なくとも4つの異なる出力を生み出す——同じプログラムの3つのバリエーションにおいて、調べた7つのランタイムのうち2つがすべての出力で一致することはない。

この記事では、Python の PEP 492 がネイティブコルーチンとジェネレータを意図的に分離し、async コードを静的に区別可能かつリンターフレンドリーにしていること、そしてさまざまなランタイム実装におけるエルゴノミクスと semantics コントロールの間のトレードオフを認めていることを論じている。
この論文は、タスクのライフサイクルの3つのフェーズにグループ化された9つの「設計次元」によって、この意見の相違をフレームワークとしている。タスクの開始フェーズの次元は、async呼び出しがどのように評価されるかを支配する——遅延評価(Python、Rust)、現在のスレッドでの熱心な評価(C#、JavaScript)、または言語によって異なるサスペンション保証。終了フェーズの次元は、タスクの範囲(無期限vs.スポーンした関数のスコープ内)、参照の強さ、およびタスクがどのようにクリーンアップされるか(awaited、キャンセル、または outright 終了)をカバーする。キャンセルの次元——認識、方向、永続性——は、タスクがキャンセルにどのように対応できるか、およびどのように対応するかを決定する。
見出しの相違を説明する2つの次元がある。ダイナミックエクステント言語(Swift、Python+Trio)は、スポーンされたタスクがそれを作成した関数の寿命を超えて存続することを許さない。しかし、そのスコープの終わりでSwiftはタスクをキャンセルし、Trioはそれを待つ——そのため、同じソーステキストに対してSwiftは「AC」を出力し、Trioは「ABC」を出力する。この論文は、これらの選択を小ステップ運用意味論で形式化し、各ランタイムがどこで分岐するかを正確に追跡する。
サポートとなる言語仕様は、これが意図的であり、偶発的ではないことを確認している。PythonのPEP 492は、ネイティブコルーチンとジェネレータを分離することで、asyncコードを静的に区別可能かつリンターフレンドリーにしている。Rustのasync/await RFC(2394)は、この機能をfutures上のエルゴノミックなシュガーとして捉えている——async関数はポーリングされるときのみ実行される匿名のFuture型を返す。両方の設計ドキュメントは、この分類法が命名するトレードオフ——エルゴノミクス vs. コントロール、パフォーマンス vs. semantics——を認めているが、いずれも普遍的に正解である単一の答えを主張していない。
実際の影響として、あるランタイムで正しく動作するコードが、移植時に静かに振る舞いを変える可能性があり、ある言語では安全に見える「fire-and-forget」パターンが、別の言語ではリーク、キャンセル、またはブロックを引き起こす可能性がある。著者たちは、これらの次元は原則的には軸ごとに設定可能だと主張しているが、実際には各言語がこの空間の一点にコミットしており、知識がきれいに転移されない——これは、Rust RFC自体が認めている懸念である。すなわち、完全にプログラム可能な軸は各ライブラリを独自のasync DSLに変えてしまうからだ。