Rustにおける並行プログラミングの初心者ガイド
Takashi Yamamoto
Infrastructure Engineer · Leapcell

並行性と並列性
多くの人々が並行性と並列性の概念を区別できないため、Rustで非同期プログラミングに飛び込む前に、まず並行性と並列性の違いを理解することが不可欠です。
オペレーティングシステムの教科書では、次の定義がよく見られます。
-
並行性とは、同じ時間間隔内で2つ以上のイベントが発生することを指します。
-
並列性とは、システムが計算または操作を同時に実行する能力を指します。
-
説明1:並行性とは、2つ以上のイベントが同じ時間間隔内で発生することを意味し、並列性とは、2つ以上のイベントがまったく同じ瞬間に発生することを意味します。
-
説明2:並行性とは、同じエンティティ上の複数のイベントを指し、並列性とは、異なるエンティティ上の複数のイベントを指します。
-
説明3:並行性とは、単一のプロセッサで複数のタスクを「同時に」処理することであり、並列性とは、分散クラスタなど、複数のプロセッサでタスクを同時に処理することです。
Golangの作成者の1人であるRob Pikeは、非常に洞察力があり直感的な説明を提供しました。
並行性とは、一度に多くのことを処理することです。並列性とは、一度に多くのことを行うことです。
並行性とは、一度に多くのタスクを処理する能力であり、並列性とは、一度に多くのタスクを実行する手法です。
タスクを複数のスレッドまたは非同期タスクに配置して処理する場合、並行性を活用しています。これらのスレッドまたは非同期タスクがマルチコアまたはマルチCPUマシンで同時に実行される場合、並列性を活用しています。ある意味で、並行性は並列性を可能にします。並行タスクを処理する能力があれば、並列性は自然に生じます。
- 並行性:任意の時点で1つの命令のみが実行されますが、複数のプロセス命令が高速で切り替わるため、同時実行のマクロ的な錯覚が生じます。ただし、ミクロレベルでは、それらは実際には同時ではありません。時間がセグメントに分割され、プロセス間で高速な交互実行が可能になります。
- 並列性:異なるプロセッサで複数の命令が同時に実行されることを指します。したがって、ミクロレベルとマクロレベルの両方で、タスクは同時に実行および処理されます。
複数のスレッドが動作している場合、システムにCPUが1つしかない場合、複数のスレッドが実際に同時に実行されることは不可能です。システムはCPU時間をセグメントに分割し、スレッドに割り当てます。あるスレッドが実行されている場合、他のスレッドは中断されます。このアプローチは並行性と呼ばれます。
システムに複数のCPUがある場合、スレッド操作は非並行になる可能性があります。あるCPUが1つのスレッドを実行している間、別のCPUは別のスレッドを実行できます。スレッドは同じCPUリソースを競合せず、同時に実行できます。これは並列性と呼ばれます。
結論:並行性と並列性の両方がマルチタスクを記述します。並行性は交互実行に関するものであり、並行レベルなどの処理能力に焦点を当てています。並列性は同時実行に関するものであり、タスク並列性などの実行戦略に焦点を当てています。
並行プログラミングモデル
プログラミング言語によって実装が異なることがわかっているため、言語間で並行モデルが異なります。特定の言語でプログラムを作成してコンパイルすると、プログラムの実行時にプロセスが占有されます。このプロセス内でスレッドを作成できます。これらはオペレーティングシステムレベルにあります。言語自体の中で、プログラマが言語レベルの機能を使用して作成したスレッドは、言語レベルのスレッドと見なされます。これらの2つのタイプ のスレッドが1対1であるかどうかは、言語の内部実装によって異なります。
- OSネイティブスレッド:たとえば、Rust言語はオペレーティングシステムによって提供されるAPIを直接呼び出すため、プログラム内のスレッド数は、使用されるOSスレッドの数と一致します。
- コルーチン:Go言語の動作と同様に、プログラム内のMスレッドは、何らかの方法でN個のオペレーティングシステムスレッドにマッピングされます。
- イベント駆動型:このモデルは、コールバックと組み合わせて使用されることがよくあります。パフォーマンスの点で非常に優れていますが、最大の問題は、_コールバック地獄_のリスクです。
- アクターモデル:メッセージパッシングに基づいて、このモデルは分解された小さなユニットで並行計算を実行します。Erlang言語のキラー機能です。
- Async/awaitモデル:このモデルは高性能で、低レベルプログラミングをサポートし、スレッドやコルーチンと同様に動作し、プログラミングモデルに大幅な変更を加える必要はありません。ただし、トレードオフとして、その内部実装メカニズムは非常に複雑です。
要するに、トレードオフを検討した後、Rustは最終的に、マルチスレッドとasync/awaitの両方を2つの並行プログラミングモデルとして提供することを選択しました。
- マルチスレッドは、OS APIを直接呼び出すことによって標準ライブラリに実装されます。実装と使用が簡単で、少数の並行タスクがあるシナリオに適しています。
- Async/await は実装がより複雑ですが、Rustは言語機能、標準ライブラリ、およびサードパーティライブラリの組み合わせを使用して、それを抽象化およびカプセル化します。これにより、開発者は基盤となる実装ロジックを気にせずにasync/awaitを使用できます。大規模な並行処理および非同期I/Oに適しています。
Rustでの非同期プログラミング
非同期プログラミングは、並行プログラミングモデルです。これにより、少数(または単一)のOSスレッドまたはCPUコアのみを必要としながら、多数のタスクを同時に実行できます。使用感の点で、最新の非同期プログラミングは、同期プログラミングとほとんど区別がつきません。
今日、多くの言語がasync
を介して非同期プログラミングをサポートしていますが、Rustの実装はいくつかの重要な点で異なります。
- FutureはRustでは遅延評価されます:それらは_ポーリング_されたときにのみ実行を開始します。フューチャを破棄すると、実行されなくなります。
Future
を、将来のある時点で実行されるようにスケジュールされたタスクと考えることができます。 - ゼロコスト抽象化:Rustで
async
を使用すると、ランタイムコストはゼロになります。これは、記述したコード(目に見えるコード)のみがパフォーマンスのオーバーヘッドを招き、async
の内部実装ではパフォーマンスのペナルティが発生しないことを意味します。たとえば、async
を使用するためにヒープメモリを割り当てる必要も、動的ディスパッチを実行する必要もありません。これはパフォーマンスに非常に役立ち、特にホットパスでは、Rustの非同期パフォーマンスが非常に高い理由の1つです。 - Rustには非同期呼び出しに必要な組み込みのランタイムは含まれていませんが、それは問題ではありません。Rustのエコシステムは、有名なTokioなどの優れたランタイム実装を提供しています。
- ランタイムはシングルスレッドモードとマルチスレッドモードの両方をサポートしています。それぞれに独自の利点とトレードオフがあり、これについては後で説明します。
Asyncとマルチスレッドの選択
async
とマルチスレッドの両方を使用して並行プログラミングを実現できます(後者はスレッドプールを介して並行性を高めることさえできます)が、これら2つのアプローチは互換性がありません。一方から他方に切り替えるには、多くの場合、大幅なコードリファクタリングが必要です。したがって、それらの違いと適用可能なシナリオを理解し、正しい選択を事前に行うことが非常に重要です。
- 並列計算などのCPU負荷の高いタスクの場合、マルチスレッドの方が有利です。これは、このようなタスクでは、スレッドが長期間フル稼働し続ける傾向があるためです。作成するスレッドの数は、並列処理機能を最大限に活用するために、CPUコアの数と同じにする必要があります。この場合、スレッドの頻繁な作成または切り替えは必要ありません。スレッドコンテキストの切り替えによってパフォーマンスのオーバーヘッドが発生するためです。スレッドを特定のCPUコアにバインドして、このオーバーヘッドを削減できます。
- Webサーバー、データベース接続、その他のネットワークサービスなどのIO負荷の高いタスクの場合、非同期プログラミングの方が有利です。これは、これらのタスクがほとんどの時間を待機に費やすためです。マルチスレッドを使用すると、ほとんどのスレッドがほとんどの時間アイドル状態になります。スレッドコンテキストの切り替えにかかるコストが高いことと相まって、これはパフォーマンスの大幅な低下につながります。
async
を使用すると、CPUとメモリの使用量を効果的に削減しながら、多数のタスクを同時に実行できます。タスクがIOまたはその他のブロッキング状態になると、すぐに譲歩し、別のタスクを実行できます。async
でのタスクの切り替えにかかるコストは、マルチスレッドでのスレッドコンテキストの切り替えよりもはるかに低くなっています。
重要な注意点:Asyncは内部的にはスレッドにも基づいています。 ただし、多数のタスクを少数のスレッドにマッピングするランタイムを介してスレッドをラップします。基本的に、多数のIOバウンドの並行イベントを少数のスレッドに投入し、イベントを通じて効率的に通信します。
このアプローチのコストは、Rustプログラムのランタイムが増加することです(ランタイムは、すべての実行可能ファイルにバンドルされるRustコードです)。これにより、コンパイルされたバイナリサイズが大幅に増加します。
2つのファイルをダウンロードすると仮定して、2つの違いを簡単な例で説明しましょう。それらを次々とダウンロードする(シリアル実行)こともできますが、それは明らかに最速のアプローチではありません。当然のことながら、マルチスレッドを使用した並列ダウンロードを考えます。
マルチスレッドプログラミングの場合:
fn download_two_files() { // タスクを実行するための2つの新しいスレッドを作成します let thread_one = thread::spawn(|| download("URL1")); let thread_two = thread::spawn(|| download("URL2")); // 両方のスレッドが完了するのを待ちます thread_one.join().expect("thread one panic"); thread_two.join().expect("thread two panic"); }
各回ダウンロードするファイルが1つか2つだけの場合、このアプローチは問題なく機能します。ただし、数百または数千のファイルを同時にダウンロードする必要がある場合に問題が発生します。各ダウンロードタスクは1つのスレッドを消費し、スレッドのリソースコストが急速に拡大します(スレッドは依然として重すぎます)。この場合、async
の使用を検討できます。
Asyncプログラミングの場合:
async fn get_two_sites_async() { // 2つの別々のフューチャーを作成します // フューチャーは、JSのPromiseと同様に、将来のある時点で実行されるようにスケジュールされたタスクと考えることができます // 両方のフューチャーが実行されると、ターゲットページが同時にダウンロードされます let future_one = download_async("URL1"); let future_two = download_async("URL2"); // 両方のフューチャーが完了するまで同時に実行します join!(future_one, future_two); }
マルチスレッドモデルと比較して、asyncはここでその利点を示しています。同じレベルの並行処理では、スレッドの作成と切り替えのコストを削減します。
まとめ
並行性と並列性はどちらもマルチタスク処理の説明です。並行性とは、タスクが順番に処理されることを指し、並列性とは、タスクが同時に処理されることを指します。並行プログラミングとは、プログラムの異なる部分が独立して実行されることを意味し、並列プログラミングとは、プログラムの異なる部分が同時に実行されることを意味します。
並行プログラミングモデルの観点から、Rustの言語設計哲学(安全性、パフォーマンス、および制御を重視)により、RustはGoのような「根本的なシンプルさ」アプローチを採用しませんでした。代わりに、マルチスレッドとasync/awaitを組み合わせることを選択しました。この利点は、より強力な制御とより高いパフォーマンスです。欠点は、複雑さが増すことです。もちろん、これはシステムプログラミング言語に期待されるトレードオフです。制御とパフォーマンスのために複雑さを使用することです。
実際、asyncとマルチスレッドは相互に排他的ではありません。多くのアプリケーションでは、両方が一緒に使用されています。async
とマルチスレッドの両方で並行プログラミングを実現できます(マルチスレッドはスレッドプールを活用して並行性を高めることさえできます)が、これら2つのモデルは相互運用できません。一方から他方に切り替えるには、大規模なコードリファクタリングが必要です。したがって、プロジェクトの初期段階で適切な並行モデルを選択することが非常に重要になります。
結論として、asyncプログラミングはIOバウンドのタスクに適しており、マルチスレッドはCPUバウンドのタスクに適しているということです。選択ルールの簡単なまとめを次に示します。
- 多数のIOタスクを同時に実行する必要がある場合は、asyncモデルを選択します。
- 実行する必要のあるIOタスクが少ない場合は、マルチスレッドを選択します。スレッドの作成と破棄のオーバーヘッドを削減する場合は、スレッドプールを使用できます。
- 並行して実行するCPU負荷の高いタスクが多数ある場合(たとえば、負荷の高い計算)、マルチスレッドモデルを選択し、スレッドの数をCPUコアの数と一致させるか、わずかに超えるようにします。
- 選択肢がそれほど重要ではない場合は、デフォルトでマルチスレッドを使用します。
当社Leapcellは、Rustプロジェクトのホスティングに最適です。
Leapcellは、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです。
多言語サポート
- Node.js、Python、Go、またはRustで開発します。
無制限のプロジェクトを無料でデプロイ
- 使用量に対してのみ支払い、リクエストや料金はかかりません。
比類のないコスト効率
- アイドル料金なしの従量課金制。
- 例:25ドルで、平均応答時間60msで694万件のリクエストをサポートします。
合理化された開発者エクスペリエンス
- 簡単なセットアップのための直感的なUI。
- 完全に自動化されたCI/CDパイプラインとGitOps統合。
- 実用的な洞察を得るためのリアルタイムのメトリックとロギング。
簡単なスケーラビリティと高性能
- 高い並行性を簡単に処理するための自動スケーリング。
- 運用上のオーバーヘッドはゼロ。構築に集中するだけです。
ドキュメントで詳細をご覧ください!
Xでフォローしてください:@LeapcellHQ