Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

なぜ Async なのか?

私たちは皆、Rust によって高速で安全なソフトウェアを書けることを気に入っています。 しかし、非同期プログラミングはこのビジョンにどのように適合するのでしょうか?

非同期プログラミング、略して async は、ますます多くのプログラミング言語でサポートされている _並行プログラミングモデル_です。 async/await 構文を通じて、通常の同期プログラミングの見た目や感覚の多くを保ちながら、 少数の OS スレッド上で多数の並行タスクを実行できます。

Async と他の並行性モデルの比較

並行プログラミングは、通常の逐次プログラミングほど成熟しておらず、「標準化」も進んでいません。その結果、言語がサポートしている並行プログラミングモデルによって、並行性の表現方法は異なります。 最も一般的な並行性モデルを簡単に概観すると、非同期プログラミングが並行プログラミングというより広い分野の中でどのように位置づけられるかを理解する助けになります。

  • OS スレッドはプログラミングモデルに変更を必要としないため、 並行性を非常に簡単に表現できます。しかし、スレッド間の同期は難しい場合があり、 パフォーマンス上のオーバーヘッドも大きくなります。 スレッドプールはこれらのコストの一部を緩和できますが、大規模な IO バウンドのワークロードをサポートするには不十分です。
  • イベント駆動プログラミングは、_コールバック_と組み合わせることで非常に高性能にできますが、 冗長で「非線形」な制御フローになりがちです。 データフローやエラー伝播を追いかけるのが難しいことがよくあります。
  • コルーチンは、スレッドと同様にプログラミングモデルの変更を必要としないため、 使いやすいものです。async と同じく、多数のタスクもサポートできます。 しかし、システムプログラミングやカスタムランタイムの実装者にとって重要な低レベルの詳細を抽象化してしまいます。
  • アクターモデルは、すべての並行計算をアクターと呼ばれる単位に分割します。 アクターは、分散システムの場合とよく似た、失敗する可能性のあるメッセージパッシングを通じて通信します。アクターモデルは効率的に実装できますが、 フロー制御やリトライロジックなど、多くの実践的な問題が未解決のまま残ります。

要約すると、非同期プログラミングは、スレッドやコルーチンの使いやすさの利点の大部分を提供しながら、 Rust のような低レベル言語に適した高性能な実装を可能にします。

Rust の Async と他の言語の比較

非同期プログラミングは多くの言語でサポートされていますが、実装によって詳細はいくつか異なります。Rust の async の実装は、いくつかの点でほとんどの言語とは異なります。

  • Future は Rust では不活性であり、poll されたときにのみ進行します。future を drop すると、 それ以上進行しなくなります。
  • Rust の async はゼロコストです。つまり、使ったものに対してだけコストを支払います。 具体的には、ヒープ割り当てや動的ディスパッチなしで async を使用でき、 これはパフォーマンスにとって大きな利点です! これにより、組み込みシステムなどの制約のある環境でも async を使用できます。
  • Rust には組み込みランタイムがありません。代わりに、ランタイムはコミュニティが保守するクレートによって提供されます。
  • Rust ではシングルスレッドおよびマルチスレッドの両方のランタイムが利用可能で、 それぞれ異なる長所と短所があります。

Rust における Async とスレッドの比較

Rust における async の主な代替手段は、OS スレッドを使うことです。これは std::thread を通じて直接使うことも、スレッドプールを通じて間接的に使うこともできます。 スレッドから async へ、またはその逆へ移行するには、 通常、実装の面でも、(ライブラリを構築している場合は)公開されるパブリックインターフェイスの面でも、大規模なリファクタリング作業が必要です。そのため、 早い段階でニーズに合ったモデルを選択すれば、多くの開発時間を節約できます。

OS スレッドは、スレッドには CPU とメモリのオーバーヘッドが伴うため、少数のタスクに適しています。 スレッドの生成やスレッド間の切り替えは非常に高コストであり、アイドル状態のスレッドでさえシステムリソースを消費します。 スレッドプールライブラリはこれらのコストの一部を緩和する助けになりますが、すべてではありません。 しかし、スレッドを使うと、大きなコード変更なしに既存の同期コードを再利用できます。特定のプログラミングモデルは必要ありません。 一部のオペレーティングシステムでは、スレッドの優先度を変更することもできます。 これは、ドライバーやその他のレイテンシに敏感なアプリケーションに役立ちます。

Async は、特にサーバーやデータベースのような大量の IO バウンドタスクを伴うワークロードにおいて、 CPU とメモリのオーバーヘッドを大幅に削減します。 他の条件が同じであれば、OS スレッドよりも桁違いに多くのタスクを扱えます。 これは、async ランタイムが少数の(高コストな)スレッドを使って多数の(低コストな)タスクを処理するためです。 しかし、async Rust では、async 関数から生成されるステートマシンと、各実行可能ファイルが async ランタイムを同梱することにより、 バイナリ blob が大きくなります。

最後に付け加えると、非同期プログラミングはスレッドよりも_優れている_わけではなく、 異なるものです。 パフォーマンス上の理由で async が必要ないのであれば、多くの場合、スレッドの方がより単純な代替手段になります。

例: 並行ダウンロード

この例では、2 つの Web ページを並行にダウンロードすることが目標です。 典型的なスレッドベースのアプリケーションでは、並行性を実現するためにスレッドを生成する必要があります。

fn get_two_sites() {
    // Spawn two threads to do work.
    let thread_one = thread::spawn(|| download("https://www.foo.com"));
    let thread_two = thread::spawn(|| download("https://www.bar.com"));

    // Wait for both threads to complete.
    thread_one.join().expect("thread one panicked");
    thread_two.join().expect("thread two panicked");
}

しかし、Web ページのダウンロードは小さなタスクです。このような少量の作業のためにスレッドを作成するのは、かなり無駄です。より大きなアプリケーションでは、 簡単にボトルネックになり得ます。async Rust では、追加のスレッドなしにこれらのタスクを並行に実行できます。

async fn get_two_sites_async() {
    // Create two different "futures" which, when run to completion,
    // will asynchronously download the webpages.
    let future_one = download_async("https://www.foo.com");
    let future_two = download_async("https://www.bar.com");

    // Run both futures to completion at the same time.
    join!(future_one, future_two);
}

ここでは、追加のスレッドは作成されません。さらに、すべての関数呼び出しは静的にディスパッチされ、 ヒープ割り当てもありません! しかし、そもそもコードを非同期に書く必要があります。 この本はそれを実現する手助けをします。

Rust におけるカスタム並行性モデル

最後に付け加えると、Rust はスレッドと async のどちらかを選ぶことを強制しません。 同じアプリケーション内で両方のモデルを使用できます。これは、 スレッドベースの依存関係と async の依存関係が混在している場合に役立つことがあります。 実際、イベント駆動プログラミングなど、まったく別の並行性モデルを使うことさえできます。 それを実装しているライブラリを見つけられる限り可能です。