4 ポイント 投稿者 GN⁺ 2025-12-04 | 1件のコメント | WhatsAppで共有
  • Zig 言語は、既存の非同期I/O設計の複雑さを下げるために、新しいIoインターフェースベースのモデルを導入
  • このモデルは同期・非同期コードを区別せず同一の関数構造を維持し、Io.ThreadedIo.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_uringkqueueなどを使用)
  • 利用者は直接新しいIo実装を作成でき、実行方式の細かな制御が可能

コード例と動作

  • 例のsaveFile()関数はファイルの作成、書き込み、クローズを実行
    • Io.Threaded使用時は通常のシステムコールとして動作
    • Io.Evented使用時は非同期バックエンドとして実行
    • どちらの場合もwriteAll()呼び出し時点で作業完了が保証される
  • 同一のコードが同期・非同期環境の双方で同じように動作
    • ライブラリ作成者は実行方式を意識する必要がない

async()concurrent()を使った並列実行

  • async()関数は非同期実行を要求するが、Io.Threadedでは即時実行されることもある
    • Io.Eventedでは実際の非同期実行で2つのファイルを同時保存可能
  • concurrent()関数は実際に並列実行が必要な場合に使用
    • Io.Threadedはスレッドプールを活用
    • Io.Eventedasync()と同じように処理
  • 間違った関数選択(asyncではなくconcurrent)はバグとみなされ、言語レベルで防ぐことはできない

コードスタイルと言語統合

  • 非同期専用構文なしで標準的なZigコードスタイルを維持
    • trydeferなど既存の制御フロー構文をそのまま使用
    • 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設計によりセキュリティ・性能・テスト制御性が向上
  • 非同期実行とスレッド実行の違い、stackfulstacklessコルーチン実装方式など技術的議論も継続
  • ZigのIoは言語レベルでの特別な処理なしに、標準ライブラリ拡張の形で実装
    • 将来、stacklessコルーチン機能の追加が予定されている

結論

  • Zigの新しい非同期モデルは言語のシンプルさの維持と高性能I/Oの両立を目指す
  • 関数の色付け問題を解決し、同期・非同期コード統合明示的制御構造を通じて
    Zig 1.0安定化の主要段階として評価される

1件のコメント

 
GN⁺ 2025-12-04
Hacker Newsのコメント
  • 全体として、この記事は正確でよく調査されている。
    ただし、いくつか小さな修正点がある。
    Io.Threaded インスタンスでは、async() は実際には非同期に動作せず、即座に実行される。だが、std.Io.Threaded はデフォルトで スレッドプール を使って非同期処理を分配する。
    ただし、init_single_threaded で初期化した場合は、記事で説明されているような動作になる。
    もうひとつ、以前は asyncConcurrent() という関数があったが、今は単に concurrent() に名前が変わっている

    • Darocです。このフィードバックを反映し、記事に 2 つの 修正 を適用した。
      今後フィードバックを送る場合は、lwn@lwn.net にメールしてほしい。
      修正案と Zig に関する作業に感謝する
    • Andrew に質問がある。
      async() を使うべき箇所で誤って asyncConcurrent() を使った場合、どんなバグが起きるのか気になる。
      IO モデルによっては UB(未定義動作) になり得るのか、それとも単なるロジックエラーなのか知りたい
    • concurrent() の良い点は、コードの可読性と表現力を高め、「このコードは必ず並列実行されるべきだ」と明確に示せることだ
  • この設計はかなり合理的だと思う。
    ただし、Zig の説明は混乱を招く。
    関数カラーリング問題を解決したと強調しているが、実際には effect type として IO を押し込んだだけにすぎない。
    これは呼び出し側がトークンを保持しなければならない形で、依然として一種のカラーリングだ。
    Go の非同期処理方式に近いと見ている

    • 引数を変えて呼ぶだけで「色付き関数」になるなら、すべての関数が色付きということになって意味がなくなる ;)
      Zig の以前の async-await モデルも、すでに coloring 問題は解決していた。
      コンパイラが呼び出しコンテキストに応じて 同期版/非同期版 を自動生成していたからだ
    • 実際、function coloring の本質的な問題は 同期/非同期コードパスの重複 だ。
      Zig はこれを 依存性注入 で解決しており、実用上は十分だ。
      async 呼び出しの複雑さは避けられないが、精密な制御のためにはやむを得ない部分でもある
    • Zig の io は伝染性のある effect type ではない。
      グローバルな io 変数を宣言してどこからでも使える(もちろん、ライブラリを書く場合には推奨されない)。
      function coloring 問題の 5 つの条件を整理した What color is your function? を見ると、Zig のアプローチは一部の条件(特に 4、5)を満たしていない可能性が高い
    • 実質的に Zig はすべてを async で色付け し、ワーカースレッドを使うかどうかだけを選ばせているように見える。
      しかし、このアプローチは デッドロック のような問題を引き起こし得る。
      一部のコードはスレッドセーフではないため、coloring がむしろ役に立つ
    • Haskell 開発者として見ると、Zig は言語サポートなしで IO モナド を実装したように見える
  • この設計は Scala の async と非常によく似ているように見える。
    Scala では実行コンテキストが 暗黙パラメータ として渡されるが、Zig では明示的に受け取る。
    実際には、スレッドやキューを直接使うより大きく優れているわけではなく、実行コンテキストの管理が 複雑で予測不能な挙動 を引き起こしていた。
    Zig チームは Scala の経験が少ないので、このアプローチを新しいものだと思ったのかもしれない

    • OS スレッドを直接使うと、リトルの法則 に従ってスケーラビリティの限界に突き当たる。
      JVM は 仮想スレッド でこれを解決するが、低レベル言語では同じ効率を出すのは難しい。
      したがって、Zig のような言語には別のスケーラビリティ解決策が必要だ
    • 参考までに、Scala の ExecutionContext API を見ると、関連する概念をよりよく理解できる
  • 以前の Zig の async/await システムでは、関数の suspend/resume が可能だった。
    この機能を使って、OS 開発時にデバイス割り込みベースの フレームの一時停止/再開 を実装してみたかった。
    新しい io システムでは、これを自分で実装しなければならなそうなのが残念だ

    • @asyncSuspend@asyncResume という 低レベル組み込み が存在する。
      新しい Io は同期、スレッド、イベントベースの共通抽象化なので、suspend の仕組みは含まれていない
    • 最終的には、suspend/resume は ユーザー空間の標準ライブラリ関数 として実装される可能性がある。
      現在の Io.Evented プロトタイプ を見ると、stackless coroutine ベースでサードパーティーライブラリから扱えるかもしれない
    • スレッドプールを 1 つだけ置いて suspend/resume を実装できるのかも気になる
    • 協調型コルーチンを プリエンプティブ async として実装することにどんな意味があるのかも疑問だ
  • サンプルコードでは writeAll() が返るときに処理が完了するとされているが、
    IO 実装はさまざまあり得るので、実際には defer が始まる時点で 完了が保証されるべきだ。
    そうでなければ、createFilewriteAll の間の 依存関係追跡 が必要になる。
    そうなると結局 blocking 呼び出し と変わらないように見える。
    また、このインターフェースが IO という名前である理由も曖昧だ。
    実際には「別のコンテキストで実行する」ための抽象化に近い
    関連文書: std.Io

  • 次の例が興味深い

    var a_future = io.async(saveFile, .{io, data, "saveA.txt"});
    var b_future = io.async(saveFile, .{io, data, "saveB.txt"});
    const a_result = a_future.await(io);
    const b_result = b_future.await(io);
    

    Rust や Python では、コルーチンは await されない限り進行しない
    一方、Zig の例で io.async がそれ自体で進行するなら、それは タスク生成 に近い。
    これは有効な設計だが、他の言語が選んだ方向ではない

    • C# も似たように動作する。async 関数は yield するまでは呼び出しスレッド上で実行 される
    • Zig でも同様に、.await(io) を呼ばないと実行は保証されない。
      即時実行されるか、スレッドプールにキューイングされるかは Io ランタイム実装 に依存する
    • 実際には await の時点で実行が進む。
      evented io の場合、2 つの処理が 交互に(interleaved) 実行される可能性があり、threaded io ではバックグラウンドで進行することもある。
      つまり、「どこかでこっそり実行されるタスク」は存在しない
    • JavaScript もこのように動作する
  • 毎日 Go を使っている立場からすると、Zig の Io は Go のさまざまな 欠点を補正 しているように感じる。
    ただし、Zig に チャネル(channel) の概念があるのかは気になる。
    Go では select キーワードがあるが、ソケットには使えないのがいつも惜しかった

    • すべての IO をチャネルで包むと コストが高い ことを指摘したい。
      Go のチャネルには数十サイクル単位のオーバーヘッドがあるので、小粒な IO には非効率だ。
      その代わり、大きな単位のデータ移動多対多の同期 には有用だ
    • Zig には Go のチャネルに似た std.Io.Queue がある。
      select 文も似た形で実装できるが、文法的にはそれほど エルゴノミック ではない。
      その代わり、GC なしでさまざまな IO ランタイム で動作できるという利点がある
    • Odin 言語を使ったことがあるか聞いてみたい。Zig よりも Go から多くの影響を受けた「better C」だ
    • C# の async/await のように 色付き関数 を強制しない点が気に入っている。
      Zig の「colorless」アプローチのほうがずっと良いと思う
    • Go の concurrency モデルが特別だと思い込むのは問題だ。
      goroutine は グリーンスレッド、チャネルは スレッドセーフなキュー にすぎず、Zig もすでにそれを標準ライブラリで提供している
  • Zig の async 版 Io は Go のアプローチとほぼ同じに見える。
    ただし Go では、C ライブラリ呼び出し時の スタック割り当てコスト が大きく、直接 syscall にはプラットフォーム互換性の問題がある。
    Zig はこれを 構成可能 にして、コード変更なしにさまざまなトレードオフを選べるようにしたようだ

  • 新しい async IO は単純な例では素晴らしいが、サーバー級の複雑な IO では限界があるかもしれない。
    関連 issue を GitHub に上げてある

  • 核心的な問題は、言語やライブラリの設計者が 異なる実行コンテキスト(sync/async) を橋渡しする手段を提供しなければならないことだ。
    そのためには、コンテキストを FSM(有限状態機械)で包み、両者の間の 通信チャネル を提供する方式が必要になる
    関連記事: Function colors represent different execution contexts