概要
Asynchrony、 Concurrency、 Parallelism の違いを明確に解説 「非同期」と「並行」の混同がもたらす問題点の指摘 Zig言語での io.async の具体的な挙動 従来のライブラリ設計・利用の課題と改善策の提案 async コードの伝播問題や設計上のベストプラクティスを紹介
非同期は並行性ではない:Zigにおける設計思想
- 多くの人が「 Concurrency is not Parallelism」と言うが、 asynchrony という概念が抜け落ちている現状
- Wikipedia の定義
- Concurrency :複数タスクを同時実行またはタイムシェアリングで進行できる能力
- Parallel computing :複数の計算や処理を物理的に同時実行する能力
- 本質的な違いを理解しないことでソフトウェア設計に悪影響
- asynchrony :タスクの実行順序が前後しても正しさが保たれる特性
- concurrency :タスクを複数同時に進行できる能力(並列またはタスク切り替えによる)
- parallelism :物理的に複数タスクを同時実行できる能力
ファイル保存とソケット接続の例
- 2つのファイルを保存する場合、どちらを先に書いても、また交互に書いても正しい( asynchrony)
- サーバーとクライアントのソケット接続では、両者の実行が重なる必要がある( concurrency 必須)
- 前者は順序の自由度があるが、後者は同時進行しなければ成立しない
なぜこの違いが重要か
- asynchrony と concurrency の区別が曖昧なため、
- ライブラリ作者が同じ機能を非同期・同期で二重実装する羽目になる
- ユーザーも「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 でエラー発生時は関数を直接実行するフォールバック設計
- リソース不足時の堅牢性向上
まとめ
- asynchrony と concurrency の違いを明確に区別する重要性
- Zigの設計思想により、同期・非同期・並行処理の柔軟な共存が可能
- async/awaitの伝播問題やライブラリの二重実装の回避
- 正しい抽象化でシンプルかつ堅牢な並行プログラミングを実現