同じasync/awaitでも言語ごとに挙動が全く違う、その設計空間を探る
A Design Space Exploration of Async/Await

async/awaitはどの言語でも同じように見えるが、実際の実行意味論は大きく異なる。Brown大学の研究チームが7つのランタイム(Asyncio、C#、JavaScript、Tokio、Smol、Trio、Swift)を比較したところ、単純なバックグラウンドログ書き込みプログラムでも4種類の出力があり、3つのバリエーションでは全て異なる結果になった。論文ではEagernessやExtentなど9つの設計次元を特定し、形式意味論で差異を説明している。
7つのランタイム全体で、この単純なプログラムの3つのバリエーションに対して、同じ出力を生成するものは2つとして存在しない!
HNでの議論
121- biorach
asyncランタイム間で重要な基本的な実装上の選択があることは以前から明らかだったが、まさか_9つ_もの設計次元があるとは。すごい。
asyncは言語の自己完結的で比較的単純な側面のように見えるという点で、人を欺くものだと思う。しかし、多くの設計上の選択が必要であり、それらはすべて広範な影響を及ぼす。
さらに、これらの次元の多くが持つ影響は完全には理解されておらず、集合的に、それらが実装でどのように展開しているかをまだ理解しようとしている最中だと思う。これに加えて、いくつかの影響の微妙な性質と組み合わせが...
良い比較は、プログラミング言語における字句スコープと動的スコープだと思う。これは、プログラミング言語設計の初期の数十年間にわたって議論された設計次元だ。時間が経ち、具体的な実装を扱うことで得られた経験によって、字句スコープがデフォルトの選択であるべきで、動的スコープは様々なニッチに制限されるべきであることが明らかになった。
- cpa
この種のセマンティクス優先のプログラミング言語の比較分析は非常に重要だ。
大学で、異なる言語が並行性、並列性、モジュール/OOP、メタプログラミング、正格評価と遅延評価、型、例外などをどのように扱うかを解剖するコースを取った。各言語が行ったトレードオフ(およびその歴史的系譜)を理解することで、Python/Java/Cのコースよりもはるかに多くのプログラミングについて学び、新しい言語を習得するのがずっと容易になった。
- galaxyLogic
(JSでの)async/awaitで私が遭遇する問題は、asyncメソッドは非async関数を呼び出してその結果で何かできるが、その逆は真ではないということだ。sync関数はasync関数を呼び出せるが、その結果をどうにかして使うことはできず、渡すか上に上げるだけだ。
さらに悪いことに、既存の呼び出し元がある場合、sync関数を単にasync関数に変更することはできない。それはおそらくそれらを壊すだろう。
これは私のプログラムにおける可能な関数呼び出しのツリー全体に影響する。あるレベルでsync関数があるが、async関数しか提供できないデータに到達する必要があるとわかった場合、そのツリー内の単一の関数を変更するだけでなく、呼び出しツリーの呼び出しチェーン全体を変更する必要があるかもしれない。
そして、プログラムを開発するにつれて、すべての関数について決定を下す必要がある。syncかasyncか?多くの場合、どちらでもあり得るので、どちらを選ぶべきか?asyncにすると、後でプログラムを進化させやすくなるように思える。しかし、すべての関数をasyncにすることは意味があるのだろうか?
- spankalee
わあ、これは本当に役に立ち、タイムリーだ!
async/awaitを備えた新しい言語を構築していて、これらの決定の多くを下さなければならなかったが、自分を根拠づけるためのこれほど組織化されたフレームワークはなかった。主にTrioで少しJavaScriptを選んだことがはっきりとわかって嬉しい。
私の言語(Zena)のasyncドキュメントページ:https://zena-lang.dev/guide/async/ 一度見直して、決定ポイントをより明示的に指摘しようと思うかもしれない。
fwiw、Trioの作者によるキャンセルに関するこの投稿は非常に説得力があると思った:https://vorpus.org/blog/timeouts-and-cancellation-for-humans... そしてZenaのキャンセル設計はそれに基づいている。
追記:キャンセルセクションにJavaScriptのAbortSignalが含まれていればよかったのにと思う。それが良いからではなく、キャンセルトークンを渡すのは存在するパターンだからだ。誰がキャンセルできるか、そしてAbortSignalのように、タスクがキャンセルチェックにオプトインしなければならないかどうかという次元もある。
- theamk
素晴らしい投稿だが、クイズは不公平だ - 唯一の「真の方法」があると仮定しているが、多くのフレームワークはオプションを提供している。
例えば、Trioには設計上グローバルな「spawn」メソッドがない。結果から判断すると、著者らは「with trio.open_nursery() as n: n.start_soon(write_to_log())」を想定しており、そのためeager実行、動的範囲、破壊的伝播を得た。
しかし、単一のログ行を書くためだけにナーサリーを開くのは全くクレイジーだ!実際のプログラムでは、適切にスコープされた共有ナーサリーを使用するだろう:リクエストごとかグローバルか。後者のオプションは無期限の範囲と「決して」伝播を可能にする。
また、write_to_logの後の「()」が重要だ!Trio自身の例に従うなら、「n.start_soon(write_to_log)」と書くだろう - ()がないことに注意。これはlazy実行に切り替わる。
リストされている非Pythonフレームワークには詳しくないが、同様に広範囲の動作を許容していても驚かない。
- jcelerier
「願わくばC++はこれらの軸を横断して選択できるようにして、目の前の問題に最適なasyncプリミティブを自分で構築できるようにしてほしい」と思っていたら:その通り!
> 表1で提供された分類法において、C++を特定の設計点に帰属させることはできない。なぜなら各軸が設定可能だからだ。エレガントで中立ではあるが、完全なプログラマビリティの選択は各ライブラリをasync DSLにする。同じ言語内のプロジェクト間の知識移転は極めて困難になる。
これらの軸と、特定の設計ではなく自分の特定の問題を解決するものを考えれば、そうではない。例えば、想像できる最も単純なプログラムを考えてみよう:ネットワークビデオプレーヤー。例えば、あるサーバーがRTPオーディオとビデオフレームを送信し、それらを正しく再生し、その上に素敵なGUIを載せる必要がある。
これを可能な限り効率的に行いたいなら、async相互運用の可能なすべての方法を認識する必要がある:
- コルーチンが輝く古典的なネットワークステートマシンでネットワークからパケットを接続して受信する
- ビデオフレームを表示するためのvsync対非vsyncの処理
- 使用したいハードウェアビデオデコーディングシステムが提供するasyncパラダイムに準拠する。例えばIntel QuickSync対VideoToolbox対NVDEC...
- プルモードで駆動されるオーディオ再生の同期モデルの処理
- オーディオ/ビデオ間の同期の処理、したがってオーディオスレッドができないためマルチスレッディングをサポートするasyncパターン [...]
- flossly
前回のプロジェクト(Kotlin; Webアプリ)では、async(コルーチン)を使わないことに決めた。
ただシンプルに保ちたかった。
asyncはコードを「感染」させる:その利点を得るには、コードベース全体がそれを行っている必要がある(リクエストの取り込み、DB呼び出し、Web API呼び出し)。
このため、プログラミング言語では「分割された」スタックが見られる:同期用のライブラリスタックと、async用のライブラリスタック。
負荷時のパフォーマンス向上の利点は、どこでもasyncを行うことの精神的オーバーヘッドに見合うとは思わなかった。だからブロッキング呼び出しと仮想スレッドを選んだ。後悔はない。
- xboxnolifes
どうやら私はasync/awaitを理解していないようだ。