0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rustの並行処理:非同期で陥りがちな落とし穴

Posted at

表紙

Rust での非同期実行には一定の複雑さがあり、プログラミング中にいくつかのミスを犯しやすいです。この記事では、Rust の非同期ランタイムでよくある落とし穴について紹介します。

意図しない同期ブロッキング

非同期コードの中で意図せず同期ブロッキング操作を行うことは、非同期プログラミングにおける重要な落とし穴の一つです。これは非同期プログラミングの利点を損ない、パフォーマンスのボトルネックとなります。以下はよくあるシナリオです:

  1. 非同期関数内でブロッキング I/O 操作を使う:例えば、async fn内で標準のstd::fs::File::openstd::net::TcpStream::connectといったブロッキング関数を直接呼び出す。
  2. 非同期クロージャ内で計算量の多い処理を行う:非同期クロージャ内で大量の CPU 計算を実行すると、現在のスレッドがブロックされ、他の非同期タスクの実行に影響を与える。
  3. 非同期コード内でブロッキングなライブラリや関数を使う:一部のライブラリや関数は非同期インターフェースを提供しておらず、同期的にしか呼び出せません。これらを非同期コード内で使うと、ブロックが発生します。

以下のコードを見て、std::thread::sleeptokio::time::sleepの違いを比較してください:

use tokio::task;
use tokio::time::Duration;

async fn handle_request() {
    println!("開始リクエスト処理");
    // tokio::time::sleep(Duration::from_secs(1)).await; // 正しい:tokioのsleepを使う
    std::thread::sleep(Duration::from_secs(1)); // 間違い:同期sleepを使ってしまう
    println!("リクエスト処理完了");
}

#[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!("すべてのリクエスト処理が完了、所要時間:{:?}", start.elapsed());
}

どうすれば同期ブロッキングの落とし穴を避けられるか?

  1. 非同期対応のライブラリと関数を使うこと:可能な限り、非同期インターフェースを提供するライブラリや関数を使いましょう。たとえば、Tokio や async-std などのランタイムが提供する非同期 I/O、タイマー、ネットワーク機能などです。

  2. 計算負荷の高い処理は別スレッドプールで実行すること:非同期コード内で計算量の多い処理が必要な場合、tokio::task::spawn_blockingasync-std::task::spawn_blockingを使って、専用のスレッドプールで実行することで、メインスレッドのブロックを避けることができます。

  3. 依存ライブラリをよく確認すること:サードパーティ製のライブラリを使う際は、非同期インターフェースが提供されているかを確認し、ブロッキング操作を取り入れてしまわないようにしましょう。

  4. ツールを使って分析すること:Tokio が提供する console などの性能分析ツールを使って、非同期コード内にブロッキング操作が存在するかを検出できます。

.await の忘却

非同期関数は Future を返すため、.await を付けて実行しなければ結果は得られません。.await を忘れると、Future は実行されずにスキップされてしまいます。

以下のコードを見てください:

async fn my_async_function() -> i32 { 42 }

#[tokio::main]
async fn main() {

    // 間違い:.await を忘れているため、関数は実行されない
    my_async_function();

    // 正しい:.await を付けて非同期関数を実行
    let result = my_async_function().await;
    println!("正しい非同期操作の結果は:{}", result);
}

spawn の乱用

軽量なタスクを頻繁に spawn すると、スケジューリングやコンテキストスイッチなどの追加オーバーヘッドが発生し、かえってパフォーマンスが低下する可能性があります。

以下の例では、各数値を 2 倍にして 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 {
        // 間違い:簡単な処理にも毎回 spawn を使う
        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;
    });
}

上記の誤った例では、単純な掛け算処理ごとに新しいタスクを spawn しており、大量のスケジューリングオーバーヘッドが発生します。正しい例では、非同期関数を直接 .await することで、余計なオーバーヘッドを回避しています。

本当に並行して処理すべきタスクにのみ spawn を使うべきです。計算量が多い、あるいは I/O が重く時間のかかる処理に対しては spawn が適しています。非常に軽量な処理であれば、直接 await する方が通常は効率的です。複数のタスクを一括で管理するには、tokio::task::JoinSet などのユーティリティも使用できます。

結論

非同期 Rust は強力ですが、使い方を誤りやすい一面もあります。ブロッキング呼び出しを避け、.await を忘れず、本当に必要なときだけ spawn を使いましょう。慎重にコードを書けば、非同期コードは高速かつ信頼性の高いものになります。


私たちはLeapcell、Rustプロジェクトのホスティングの最適解です。

Leapcell

Leapcellは、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです:

複数言語サポート

  • Node.js、Python、Go、Rustで開発できます。

無制限のプロジェクトデプロイ

  • 使用量に応じて料金を支払い、リクエストがなければ料金は発生しません。

比類のないコスト効率

  • 使用量に応じた支払い、アイドル時間は課金されません。
  • 例: $25で6.94Mリクエスト、平均応答時間60ms。

洗練された開発者体験

  • 直感的なUIで簡単に設定できます。
  • 完全自動化されたCI/CDパイプラインとGitOps統合。
  • 実行可能なインサイトのためのリアルタイムのメトリクスとログ。

簡単なスケーラビリティと高パフォーマンス

  • 高い同時実行性を容易に処理するためのオートスケーリング。
  • ゼロ運用オーバーヘッド — 構築に集中できます。

ドキュメントで詳細を確認!

Try Leapcell

Xでフォローする:@LeapcellHQ


ブログでこの記事を読む

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?