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

[![表紙](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/67696d6b-6869-43ff-83fa-0e6cfb2bac81.png)](https://leapcell.io/?lc_t=q_rustasyncpitfall)

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

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

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

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

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

```rust
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_blocking`や`async-std::task::spawn_blocking`を使って、専用のスレッドプールで実行することで、メインスレッドのブロックを避けることができます。

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

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

## .await の忘却

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

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

```rust
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` に格納し、最終的に要素数を出力します。誤ったやり方と正しいやり方の両方を示します：

```rust
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プロジェクトのホスティングの最適解です。](https://leapcell.io/?lc_t=q_rustasyncpitfall)

[![Leapcell](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/042cc3f0-9b64-443f-a3ad-f019ffd0e237.png)](https://leapcell.io/?lc_t=q_rustasyncpitfall)

[Leapcell](https://leapcell.io/?lc_t=q_rustasyncpitfall)は、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです：

**複数言語サポート**

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

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

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

**比類のないコスト効率**

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

**洗練された開発者体験**

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

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

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

[ドキュメントで詳細を確認！](https://docs.leapcell.io/)

[![Try Leapcell](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/2fb9a975-3929-4d40-b7ed-eb7c782f7904.png)](https://leapcell.io/?lc_t=q_rustasyncpitfall)

Xでフォローする：[@LeapcellHQ](https://x.com/LeapcellHQ)

---

[ブログでこの記事を読む](https://leapcell.io/blog/common-async-pitfalls-in-rust-concurrency)