- Zig 言語は、既存の非同期I/O設計の複雑さを下げるために、新しい
Ioインターフェースベースのモデルを導入
- このモデルは同期・非同期コードを区別せず同一の関数構造を維持し、
Io.ThreadedとIo.Eventedの2つの実装を提供
Io.Threadedはデフォルトで同期実行を、Io.Eventedはイベントループベースの非同期実行を実行
- 開発者は
async()とconcurrent()関数で並列実行を制御でき、コード変更なしで性能最適化が可能
- このアプローチは関数の色付け(function coloring)問題を解決し、Zigのシンプルさと制御性を保ちながら非同期性能を確保する方向
Zigの非同期設計の変化
- Zigは従来の非同期設計が言語のミニマリズム哲学とよく合わないため、新しいアプローチを模索
- 従来の設計は他の機能との統合性が低かった
- 新モデルは同期・非同期I/Oを同一のコード構造で処理可能
- 新設計は**
Ioジェネリックインターフェース**を中心に動作
- すべてのI/O関数は
Ioインスタンスをパラメータとして受け取って実行
Allocatorインターフェースと同様の構造で、メモリ割り当てと同じ方式でI/O制御が可能
Ioインターフェースの構造
- 標準ライブラリに2種類の基本実装が含まれる
Io.Threaded: デフォルトで同期実行、必要に応じてスレッド並列処理
Io.Evented: イベントループベースの非同期実行(io_uring、kqueueなどを使用)
- 利用者は直接新しい
Io実装を作成でき、実行方式の細かな制御が可能
コード例と動作
- 例の
saveFile()関数はファイルの作成、書き込み、クローズを実行
Io.Threaded使用時は通常のシステムコールとして動作
Io.Evented使用時は非同期バックエンドとして実行
- どちらの場合も
writeAll()呼び出し時点で作業完了が保証される
- 同一のコードが同期・非同期環境の双方で同じように動作
async()とconcurrent()を使った並列実行
async()関数は非同期実行を要求するが、Io.Threadedでは即時実行されることもある
Io.Eventedでは実際の非同期実行で2つのファイルを同時保存可能
concurrent()関数は実際に並列実行が必要な場合に使用
Io.Threadedはスレッドプールを活用
Io.Eventedはasync()と同じように処理
- 間違った関数選択(
asyncではなくconcurrent)はバグとみなされ、言語レベルで防ぐことはできない
コードスタイルと言語統合
- 非同期専用構文なしで標準的なZigコードスタイルを維持
try、deferなど既存の制御フロー構文をそのまま使用
- Andrew Kelleyは「標準のZigコードのように読める」と述べた
- 例として非同期DNSルックアップ実装を提示
getaddrinfo()と異なり、最初の成功応答だけを返し、残りのリクエストはキャンセル
今後の計画と開発状況
Io.Eventedはまだ実験的段階で、いくつかのOSが未対応
- WebAssembly互換
Io実装が計画中で、関連機能の開発が必要
Io関連の24件のフォローアップ項目があり、そのほとんどが未完了状態
- Zigはまだ1.0版前で、非同期I/Oとネイティブコード生成が主要な主要な未解決課題
- この設計でI/Oインターフェース変更によるコード再記述の頻度低減が期待
コミュニティの議論まとめ
- 複数のコメントで、ZigのアプローチはRustの
async/awaitモデルよりシンプルで柔軟という評価
- Rustでは複数のexecutorを混在使用すると複雑性が高まる
- Zigは
Ioインターフェースで複数executorの共存可能性を確保
- 一部はコードがやや冗長になる可能性を指摘
- しかし明示的なAPI設計によりセキュリティ・性能・テスト制御性が向上
- 非同期実行とスレッド実行の違い、
stackful対stacklessコルーチン実装方式など技術的議論も継続
- Zigの
Ioは言語レベルでの特別な処理なしに、標準ライブラリ拡張の形で実装
- 将来、
stacklessコルーチン機能の追加が予定されている
結論
- Zigの新しい非同期モデルは言語のシンプルさの維持と高性能I/Oの両立を目指す
- 関数の色付け問題を解決し、同期・非同期コード統合、明示的制御構造を通じて
Zig 1.0安定化の主要段階として評価される
1件のコメント
Hacker Newsのコメント
全体として、この記事は正確でよく調査されている。
ただし、いくつか小さな修正点がある。
Io.Threadedインスタンスでは、async()は実際には非同期に動作せず、即座に実行される。だが、std.Io.Threadedはデフォルトで スレッドプール を使って非同期処理を分配する。ただし、
init_single_threadedで初期化した場合は、記事で説明されているような動作になる。もうひとつ、以前は
asyncConcurrent()という関数があったが、今は単にconcurrent()に名前が変わっている今後フィードバックを送る場合は、lwn@lwn.net にメールしてほしい。
修正案と Zig に関する作業に感謝する
async()を使うべき箇所で誤ってasyncConcurrent()を使った場合、どんなバグが起きるのか気になる。IO モデルによっては UB(未定義動作) になり得るのか、それとも単なるロジックエラーなのか知りたい
concurrent()の良い点は、コードの可読性と表現力を高め、「このコードは必ず並列実行されるべきだ」と明確に示せることだこの設計はかなり合理的だと思う。
ただし、Zig の説明は混乱を招く。
関数カラーリング問題を解決したと強調しているが、実際には effect type として IO を押し込んだだけにすぎない。
これは呼び出し側がトークンを保持しなければならない形で、依然として一種のカラーリングだ。
Go の非同期処理方式に近いと見ている
Zig の以前の async-await モデルも、すでに coloring 問題は解決していた。
コンパイラが呼び出しコンテキストに応じて 同期版/非同期版 を自動生成していたからだ
Zig はこれを 依存性注入 で解決しており、実用上は十分だ。
async 呼び出しの複雑さは避けられないが、精密な制御のためにはやむを得ない部分でもある
グローバルな io 変数を宣言してどこからでも使える(もちろん、ライブラリを書く場合には推奨されない)。
function coloring 問題の 5 つの条件を整理した What color is your function? を見ると、Zig のアプローチは一部の条件(特に 4、5)を満たしていない可能性が高い
しかし、このアプローチは デッドロック のような問題を引き起こし得る。
一部のコードはスレッドセーフではないため、coloring がむしろ役に立つ
この設計は Scala の async と非常によく似ているように見える。
Scala では実行コンテキストが 暗黙パラメータ として渡されるが、Zig では明示的に受け取る。
実際には、スレッドやキューを直接使うより大きく優れているわけではなく、実行コンテキストの管理が 複雑で予測不能な挙動 を引き起こしていた。
Zig チームは Scala の経験が少ないので、このアプローチを新しいものだと思ったのかもしれない
JVM は 仮想スレッド でこれを解決するが、低レベル言語では同じ効率を出すのは難しい。
したがって、Zig のような言語には別のスケーラビリティ解決策が必要だ
以前の Zig の async/await システムでは、関数の suspend/resume が可能だった。
この機能を使って、OS 開発時にデバイス割り込みベースの フレームの一時停止/再開 を実装してみたかった。
新しい io システムでは、これを自分で実装しなければならなそうなのが残念だ
@asyncSuspendと@asyncResumeという 低レベル組み込み が存在する。新しい Io は同期、スレッド、イベントベースの共通抽象化なので、suspend の仕組みは含まれていない
現在の Io.Evented プロトタイプ を見ると、stackless coroutine ベースでサードパーティーライブラリから扱えるかもしれない
サンプルコードでは
writeAll()が返るときに処理が完了するとされているが、IO 実装はさまざまあり得るので、実際には defer が始まる時点で 完了が保証されるべきだ。
そうでなければ、
createFileとwriteAllの間の 依存関係追跡 が必要になる。そうなると結局 blocking 呼び出し と変わらないように見える。
また、このインターフェースが IO という名前である理由も曖昧だ。
実際には「別のコンテキストで実行する」ための抽象化に近い
関連文書: std.Io
次の例が興味深い
Rust や Python では、コルーチンは await されない限り進行しない。
一方、Zig の例で
io.asyncがそれ自体で進行するなら、それは タスク生成 に近い。これは有効な設計だが、他の言語が選んだ方向ではない
async関数は yield するまでは呼び出しスレッド上で実行 される.await(io)を呼ばないと実行は保証されない。即時実行されるか、スレッドプールにキューイングされるかは Io ランタイム実装 に依存する
awaitの時点で実行が進む。evented io の場合、2 つの処理が 交互に(interleaved) 実行される可能性があり、threaded io ではバックグラウンドで進行することもある。
つまり、「どこかでこっそり実行されるタスク」は存在しない
毎日 Go を使っている立場からすると、Zig の Io は Go のさまざまな 欠点を補正 しているように感じる。
ただし、Zig に チャネル(channel) の概念があるのかは気になる。
Go では select キーワードがあるが、ソケットには使えないのがいつも惜しかった
Go のチャネルには数十サイクル単位のオーバーヘッドがあるので、小粒な IO には非効率だ。
その代わり、大きな単位のデータ移動 や 多対多の同期 には有用だ
std.Io.Queueがある。select 文も似た形で実装できるが、文法的にはそれほど エルゴノミック ではない。
その代わり、GC なしでさまざまな IO ランタイム で動作できるという利点がある
Zig の「colorless」アプローチのほうがずっと良いと思う
goroutine は グリーンスレッド、チャネルは スレッドセーフなキュー にすぎず、Zig もすでにそれを標準ライブラリで提供している
Zig の async 版 Io は Go のアプローチとほぼ同じに見える。
ただし Go では、C ライブラリ呼び出し時の スタック割り当てコスト が大きく、直接 syscall にはプラットフォーム互換性の問題がある。
Zig はこれを 構成可能 にして、コード変更なしにさまざまなトレードオフを選べるようにしたようだ
新しい async IO は単純な例では素晴らしいが、サーバー級の複雑な IO では限界があるかもしれない。
関連 issue を GitHub に上げてある
核心的な問題は、言語やライブラリの設計者が 異なる実行コンテキスト(sync/async) を橋渡しする手段を提供しなければならないことだ。
そのためには、コンテキストを FSM(有限状態機械)で包み、両者の間の 通信チャネル を提供する方式が必要になる
関連記事: Function colors represent different execution contexts