Rustコンカレンシーにおける一般的な非同期な落とし穴
Ethan Miller
Product Engineer · Leapcell

非同期プログラミングには特定の複雑さが伴い、Rustでasyncを使用する際に間違いを犯しやすいです。この記事では、Rustの非同期ランタイムにおける一般的な落とし穴について説明します。
予期しない同期的ブロッキング
非同期コード内で誤って同期的ブロッキング操作を実行することは、大きな落とし穴です。これは非同期プログラミングの利点を損ない、パフォーマンスのボトルネックを引き起こします。一般的なシナリオをいくつか紹介します。
- async関数でブロッキングI/O操作を使用する: たとえば、
std::fs::File::open
やstd::net::TcpStream::connect
のような標準的なブロッキング関数をasync fn
内で直接呼び出す。 - asyncクロージャ内でCPU負荷の高いタスクを実行する: asyncクロージャ内で重い計算を実行すると、現在のスレッドがブロックされ、他のasyncタスクの実行に影響を与える可能性があります。
- asyncコードでブロッキングライブラリまたは関数を使用する: 一部のライブラリはasyncインターフェースを提供しておらず、同期的にしか呼び出すことができません。これらをasyncコードで使用すると、ブロッキングが発生する可能性があります。
std::thread::sleep
とtokio::time::sleep
の使用の違いを比較するために、次のコードを見てください。
use tokio::task; use tokio::time::Duration; async fn handle_request() { println!("Start processing request"); // tokio::time::sleep(Duration::from_secs(1)).await; // 正しい: tokio::time::sleepを使用 std::thread::sleep(Duration::from_secs(1)); // 不正解: std::thread::sleepを使用 println!("Request processing completed"); } #[tokio::main(flavor = "current_thread")] // シングル スレッド モードで tokio::main マクロを使用 async fn main() { let start = std::time::Instant::now(); // 複数の同時タスクを起動 let handles = (0..10).map(|_| { task::spawn(handle_request()) }).collect::<Vec<_>>(); // 必要に応じて、すべてのタスクが完了するまで待機 for handle in handles { handle.await.unwrap(); } println!("All requests completed, elapsed time: {:?}", start.elapsed()); }
同期的ブロッキングの罠を回避する方法は?
- 非同期ライブラリと関数を使用する:
tokio
やasync-std
のようなランタイムによって提供される非同期I/O、タイマー、ネットワーキングなど、asyncインターフェースを提供するライブラリを優先します。 - CPU負荷の高いタスクを専用のスレッドプールにオフロードする: asyncコードで重い計算が必要な場合は、
tokio::task::spawn_blocking
またはasync-std::task::spawn_blocking
を使用して、これらのタスクを別のスレッドプールに移動し、メインスレッドのブロッキングを回避します。 - 依存関係を注意深く確認する: サードパーティライブラリを使用する場合は、ブロッキング操作の導入を避けるために、asyncインターフェースを提供しているかどうかを確認します。
- 分析ツールを使用する: パフォーマンス分析ツールは、asyncコードでのブロッキング操作の検出に役立ちます。たとえば、
tokio
はconsole
と呼ばれるツールを提供しています。
.await
を忘れる
非同期関数はFuture
を返し、実際に実行して結果を取得するには.await
を使用する必要があります。.await
の使用を忘れると、Future
がまったく実行されません。
次のコードを検討してください。
async fn my_async_function() -> i32 { 42 } #[tokio::main] async fn main() { // 不正解: `.await` を忘れた場合、関数は実行されません my_async_function(); // 正しい let result = my_async_function().await; println!("The result of the correct async operation is: {}", result); }
spawn
の過剰使用
軽量タスクを過剰に生成すると、タスクのスケジューリングとコンテキストスイッチングによるオーバーヘッドが発生し、実際にはパフォーマンスが低下する可能性があります。
以下の例では、各数に2を掛け、結果をVec
に格納し、最後にVec
内の要素数を出力します。不正解と正解の両方のアプローチを示します。
use async_std::task; async fn process_item(item: i32) -> i32 { // 非常に単純な操作 item * 2 } async fn bad_use_of_spawn() { let mut results = Vec::new(); for i in 0..10000 { // 不正解: 各単純な操作に対してタスクを生成する let handle = task::spawn(process_item(i)); results.push(handle.await); } println!("{:?}", results.len()); } async fn good_use_of_spawn() { let mut results = Vec::new(); for i in 0..10000 { results.push(process_item(i).await); } println!("{:?}", results.len()); } fn main() { task::block_on(async { bad_use_of_spawn().await; good_use_of_spawn().await; }); }
上記の不正解の例では、単純な乗算ごとに新しいタスクが生成されるため、タスクのスケジューリングによる大規模なオーバーヘッドが発生します。正しいアプローチは、async関数を直接awaitし、余分なオーバーヘッドを回避します。
真の並行性が必要な場合にのみspawn
を使用する必要があります。CPU負荷の高いタスクまたは長時間実行されるI/Oバウンドタスクの場合、spawn
が適切です。非常に軽量なタスクの場合、通常は.await
を直接使用する方が効率的です。tokio::task::JoinSet
を使用して、複数のタスクをより効果的に管理することもできます。
結論
Async Rustは強力ですが、誤用しやすいです。ブロッキング呼び出しを避け、.await
を忘れず、必要な場合にのみspawnしてください。注意して記述すれば、asyncコードは高速で信頼性の高い状態を維持できます。
Rustプロジェクトをホストするための最適な選択肢であるLeapcellをご紹介します。
Leapcell は、Web ホスティング、非同期タスク、および Redis 向けの次世代サーバーレス プラットフォームです。
多言語サポート
- Node.js、Python、Go、または Rust で開発します。
無制限のプロジェクトを無料でデプロイ
- 使用量に対してのみ支払い - リクエストも料金もありません。
他に類を見ないコスト効率
- アイドル料金なしの従量課金制。
- 例: 25 ドルで、平均応答時間 60 ミリ秒で 694 万件のリクエストをサポートします。
合理化された開発者エクスペリエンス
- 簡単なセットアップのための直感的な UI。
- 完全に自動化された CI/CD パイプラインと GitOps の統合。
- 実用的な洞察を得るためのリアルタイムのメトリクスとロギング。
簡単なスケーラビリティと高いパフォーマンス
- 高い同時実行性を容易に処理するための自動スケーリング。
- 運用上のオーバーヘッドはゼロ - 構築に集中するだけです。
詳細については、ドキュメントをご覧ください。
X でフォローしてください: @LeapcellHQ