世界を動かす技術を、日本語で。

非同期性は同時実行性ではない

2025年7月19日原文(kristoff.it)

概要

AsynchronyConcurrencyParallelism の違いを明確に解説 「非同期」と「並行」の混同がもたらす問題点の指摘 Zig言語での io.async の具体的な挙動 従来のライブラリ設計・利用の課題と改善策の提案 async コードの伝播問題や設計上のベストプラクティスを紹介

非同期は並行性ではない:Zigにおける設計思想

  • 多くの人が「 Concurrency is not Parallelism」と言うが、 asynchrony という概念が抜け落ちている現状
  • Wikipedia の定義
    • Concurrency :複数タスクを同時実行またはタイムシェアリングで進行できる能力
    • Parallel computing :複数の計算や処理を物理的に同時実行する能力
  • 本質的な違いを理解しないことでソフトウェア設計に悪影響
  • asynchrony :タスクの実行順序が前後しても正しさが保たれる特性
  • concurrency :タスクを複数同時に進行できる能力(並列またはタスク切り替えによる)
  • parallelism :物理的に複数タスクを同時実行できる能力

ファイル保存とソケット接続の例

  • 2つのファイルを保存する場合、どちらを先に書いても、また交互に書いても正しい( asynchrony
  • サーバーとクライアントのソケット接続では、両者の実行が重なる必要がある( concurrency 必須)
  • 前者は順序の自由度があるが、後者は同時進行しなければ成立しない

なぜこの違いが重要か

  • asynchronyconcurrency の区別が曖昧なため、
    • ライブラリ作者が同じ機能を非同期・同期で二重実装する羽目になる
    • ユーザーも「asyncコードは伝染する」現象に悩まされる
    • 無理な回避策でデッドロックやパフォーマンス劣化を招く
  • 正しく区別することで、
    • ライブラリの重複や「async専用」設計の必要がなくなる
    • 通常の同期コードと非同期コードが共存可能

Zigにおける asynchrony と concurrency

  • io.async は concurrency を必ずしも意味しない
    • 単一スレッドのブロッキングモードでも動作可能
    • 例:
      • io.async(saveFileA, .{io}); io.async(saveFileB, .{io});
      • 単一スレッドなら saveFileA(io); saveFileB(io); と等価
  • io.async を使っても、ユーザーは同期I/Oのまま利用可能
  • 逆に、 io.async を使わなくても concurrency を利用可能
  • concurrency の本質は、
    • イベント駆動I/Oシステムコール(io_uring, epollなど)の利用
    • タスク切り替えプリミティブ(yield等)の利用
  • asyncはasynchronyのためのものであり、task switchingはconcurrencyのためのもの

タスク切り替え(yield)の仕組み

  • グリーンスレッドの場合、yieldはスタックスワッピングで実現
    • CPUレジスタやスタックの状態を保存・復元
    • OSのスレッドスケジューリングと同様の仕組み
  • イベントループはI/O完了を待つ間、他のタスクに切り替え
  • これにより、同期的に書かれたコードも並行的に実行可能

同期・非同期コードの共存

  • saveDataのような同期的な関数も、io.asyncで包めば非同期的にスケジューリング可能
  • 通常の同期コードと非同期コードが同一プログラム内で混在・共存
  • Go言語のgoroutineの例のように、async/awaitキーワードの伝播問題が発生しにくい設計

concurrencyが必須となるケース

  • サーバーacceptとクライアントconnectのように、両タスクの重なりが必須な場合
  • Zigでは io.asyncConcurrent を使い、明示的にconcurrencyを保証
    • concurrencyを必要とすることをコード上で明示
    • 非concurrentなIo実装で実行時エラーを発生させることで安全性を担保
  • io.async でエラー発生時は関数を直接実行するフォールバック設計
    • リソース不足時の堅牢性向上

まとめ

  • asynchronyconcurrency の違いを明確に区別する重要性
  • Zigの設計思想により、同期・非同期・並行処理の柔軟な共存が可能
  • async/awaitの伝播問題やライブラリの二重実装の回避
  • 正しい抽象化でシンプルかつ堅牢な並行プログラミングを実現

Hackerたちの意見

個人的には、著者は同時実行の定義を混同していると思う。 https://lamport.azurewebsites.net/pubs/time-clocks.pdf

論文をリンクするんじゃなくて、もうちょっと詳しく説明してくれない? 定義はまあまあ良いと思ったけど。 > 非同期性:タスクが順不同で実行されても正しい可能性。 > 同時実行性:システムが複数のタスクを同時に進行させる能力、並列性やタスクの切り替えを通じて。 > 並列性:システムが物理レベルで複数のタスクを同時に実行する能力。

だから、僕はこの用語を完全に使うのをやめたんだ。話す相手みんなが違う理解をしてる気がするし、コミュニケーションに全く役立たなくなった。

著者は、自分がブログ記事で使っている用語の定義が存在することを理解している。彼は改訂された定義を提案しているんだ。新しい定義が正確であれば、問題ないと思う。それを採用するかどうかは読者次第だね。

ランポートの論文の半分の概念をほとんどの言語で表現できないことを忘れないで。スレッドを開始するときに、全体的な時計の順序や部分的な時計の順序について話すことはないよね。プロトコルを設計する時にTLA+でしかやらない。とはいえ、「Zigには非同期APIに、非同時実行で実行するとコンパイルエラーを投げる関数がある。Zigはそれを言わせてくれる」という新しい用語を表現する必要はないと思う。それを新しい理論を提案せずにやるのは全然問題ないよ。

新しいZigのI/Oアイデアは、主にアプリケーションを書く人にはかなり画期的なアイデアに思える。スタックレスコルーチンが必要ないならね。でも、このスタイルでライブラリを書くのはかなりエラーが出やすいと思う。ライブラリの作者が提供されるI/Oがシングルスレッドかマルチスレッドか、イベント駆動I/Oを使っているかどうかもわからないから。並行/非同期/並列/なんでもいいけど、コードを書くのはそれ自体で十分難しいのに、I/Oスタックについて完璧な知識があってもそうなんだ。ここでは、ライブラリの作者は外部から提供されるI/O実装に振り回されることになる。I/Oインターフェースが「小さなOS」の実装みたいなものになるとしたら、すべての潜在的な相互作用や動作の組み合わせをテストするのはかなり難しいかもしれない。インターフェースが提供するいくつかの非同期プリミティブが、実際に遭遇する面白いエッジケースに対処するには十分かどうかはわからない。このような幅広いI/O実装をサポートするためには、コードはかなり防御的で、基本的に最も並列的/同時実行的なI/Oが使われることを前提にしなければならないと思う。スタックレスコルーチンとこのアプローチを組み合わせるのも難しいだろうし、特に無駄にコルーチンを生成しないようにしたいなら、提供されるプリミティブではコルーチンの明示的なポーリングを表現できないみたいだから(仮にできたとしても、ほとんどの人はそんなコードを書く気にならないだろうし、結局「普通の」非同期/awaitコードみたいに見えるだけだと思う)。動的ディスパッチと組み合わせると、Zigは言語設計で少し高レベルに進んでいるように見える。最終的には良いフィットになるかもね。このアプローチを「妥協なし」と呼ぶのはかなり勇気がいることだと思うけど、まだ実際に使われてないからね。広いエコシステムで1〜2年使った後に言えることだと思う。時間が経てばわかるよ :)

このアプローチを「妥協なし」と呼ぶのはかなり勇気がいることだと思う。まだ実際には試されていないからね。君の言う通りだ。とはいえ、「Jai」が成功しているとされるものにかなり近いように見える(暗黙のIOコンテキストを使っているけど、明示的に渡されるものではない)。でも、それを実際に試されていると言えるかどうかは議論の余地があるよね…

コードはかなり防御的で、基本的には最も並行・同時実行のIOバージョンを使うことを前提にしなければならないと思う。まさにその通りだけど、なぜ誰もが異なる考えを持つと思うのか、同期と非同期の実行の両方をサポートすることが目標なのに?でも、非同期性がIOイベントハンドラーの低レベルでうまく行われていれば、どこでもこれらの原則に従って簡単に実装できるはずだよ。最悪でもコードが逐次実行される(つまり遅くなる)だけで、レースやデッドロックにはならないはず。

僕は、著者が単に同時実行の定義から実行のyieldの概念を引き抜いて、新しい「非同期性」という用語に持っていっただけだと思う。そして、その用語が必要だと主張してるけど、実際には同時実行の概念が壊れてしまう。確かにそうだけど、yieldできる能力なしに同時実行はあまり意味がないから、実際にはそれに内在していると思う。これはとても重要な概念だけど、新しい用語に分けることで混乱を招くだけだと思う。

純粋な1対1の並列性は、yieldを伴わない同時実行の一形態としてカウントするよ。でも、それ以外は、すべての非並列同時実行は、たとえそれが命令レベルであっても、何らかのリズムで実行をyieldしなければならないに同意する。 (例えば、CUDAでは、ワープ内の分岐するスレッドが命令の実行をインターリーブするので、一方の分岐が他方でブロックしようとする場合がある。)

著者は、単に同時実行の定義から実行の譲渡という概念を引っ張り出して、この新しい「非同期」という用語に持ち込んだだけだと思う。記事には正反対のことが書いてあるけどね。>(そしてタスクスイッチングは、上で述べた定義によれば、同時実行特有の概念です)

同時実行は譲渡を意味しないよ… 同期ロジックは何らかの同期を含むし、譲渡は同期の一つの方法かもしれない。それが君の言いたいことだと思う。非同期ロジックは、同期や譲渡なしで同時実行を実現してる。実際のところ、同時実行と非同期ロジックはフォン・ノイマンマシンには存在しないんだよね。

Hacker Newsで議論の続きを見る