以下の内容はhttps://syu-m-5151.hatenablog.com/entry/2025/11/18/155416より取得しました。


上手に待つ技術:Rust Edition 2024で学ぶ非同期処理入門

はじめに

プログラミングにおいて「待つ」処理は避けられません。サーバからのレスポンスを待つ、データベースの処理が終わるのを待つ、ファイルの読み込みが完了するのを待つ——この「待ち時間」の使い方が、プログラムの性能を大きく左右します。

非同期処理とは、ある処理の完了を待たずに次の処理を開始する技術です。待っている間に他のタスクを処理することで、限られたリソースを効率的に活用できます。

本記事では、以下の内容を解説します。

  • Bashでの基本的な非同期処理
  • Rust Edition 2024における非同期プログラミングの基礎
  • 実践的なコード例とパターン

実際に動作するコード例を通じて、非同期処理の基礎から実践的なパターンまでを学んでいきましょう。


Bashでの非同期処理

シェルスクリプトでも基本的な非同期処理が可能です。まずは簡単な例から見ていきましょう。

#!/bin/bash

# バックグラウンドで実行
echo "タスク1を開始..."
sleep 3 &
pid1=$!

echo "タスク2を開始..."
sleep 2 &
pid2=$!

echo "タスク3を開始..."
sleep 4 &
pid3=$!

# すべてのタスクの完了を待つ
echo "すべてのタスクが完了するのを待っています..."
wait $pid1
echo "タスク1が完了しました"

wait $pid2
echo "タスク2が完了しました"

wait $pid3
echo "タスク3が完了しました"

echo "すべて完了!"

このスクリプトでは、& をコマンドの最後につけることで、そのコマンドをバックグラウンドで実行しています。wait コマンドで特定のプロセスの終了を待ちます。

実用的な例:複数サーバの監視

より実用的な例として、複数のサーバの死活監視を同時に行うスクリプトを見てみましょう。

#!/bin/bash

check_server() {
    local server=$1
    local start_time=$(date +%s)
    
    if ping -c 1 -W 2 "$server" > /dev/null 2>&1; then
        local end_time=$(date +%s)
        local duration=$((end_time - start_time))
        echo "$server: OK (${duration}秒)"
    else
        echo "$server: 到達不可"
    fi
}

# 複数のサーバを同時にチェック
servers=("google.com" "github.com" "stackoverflow.com" "rust-lang.org")

for server in "${servers[@]}"; do
    check_server "$server" &
done

# すべての完了を待つ
wait

echo "すべてのチェックが完了しました"

順次実行すれば8秒かかるところを、並行実行で2秒程度に短縮できます。

Bashの非同期処理の限界

ただし、Bashの非同期処理には以下のような限界があります。

  • プロセス単位での並行処理のため、オーバーヘッドが大きい
  • エラーハンドリングが煩雑
  • 状態の共有が難しい
  • 細かい制御ができない

より洗練された非同期処理には、プログラミング言語レベルでのサポートが必要となります。


なぜ非同期処理が重要なのか

ハードウェアの性能向上の鈍化

ハードウェアの性能向上は、2010年代以降、鈍化しています。NVIDIAのCEO Jensen Huangが2017年5月のCOMPUTEX TAIPEIで「ムーアの法則は死んだ」と述べました。単純にクロック速度を上げることで性能を向上させる時代は終わったのです。

つまり、これ以上は単に「速いコンピュータを買えばいい」という解決策が使えなくなってきています。

ソフトウェア要求の増大

一方で、ソフトウェアに対する要求は増大し続けています。

  • マイクロサービスアーキテクチャでは、システム内でのI/O呼び出しの回数が激増
  • Webアプリケーションは、複数のAPIを並行して呼び出す必要がある
  • モバイルアプリは、限られたリソースで複数のタスクを処理しなければならない

ここで重要になるのが非同期プログラミングです。

並行処理・並列処理・非同期処理の違いと使い分け

これらの用語は混同されやすいですが、それぞれ異なる概念を指します。

並行処理(Concurrency)

複数のタスクが論理的に同時進行しているように見せる技術です。シングルコアCPUでも実現可能で、タスクを高速に切り替えながら実行します。

例えば、レストランで一人のシェフが玉ねぎを炒めている間にトマトを切る動作が並行処理に相当します。

参考:fastapi.tiangolo.com

並列処理(Parallelism)

複数のタスクが物理的に同時実行される技術です。マルチコアCPUを活用し、実際に複数の処理が同じ瞬間に実行されます。複数のシェフがそれぞれ別の料理を作る状態が並列処理に相当します。

目的は処理速度の向上です。

参考:freak-da.hatenablog.com

非同期処理(Asynchronous)

ある処理の完了を待たずに次の処理を開始する技術です。I/O操作のような「待ち時間」が多い処理に特に有効で、ネットワークリクエストのレスポンスを待っている間に他の処理を進められます。

目的は待ち時間の有効活用です。

まとめ

これらは異なる次元の概念であり、組み合わせて使用できます。非同期処理を並行的に実行したり、並列に実行したりできます。

CPUのコア数を増やさなくても、非同期プログラミングを用いれば性能を向上させることができます。サーバからのレスポンスを待っている時間があるなら、その間に他のタスクを処理すればよいのです。

参考:qiita.com


Rust Edition 2024における非同期処理

Rustの非同期処理は、2025年2月20日にリリースされたRust 1.85.0で、Edition 2024が安定化されました。これはRust史上最大規模のEditionとなりました。

参考:blog.rust-lang.org

Edition 2024の哲学

Rust Editionは、Rustの後方互換性を保ちながら破壊的変更を導入するための仕組みです。Rust 1.0がリリースされた際、チームは「1.xのどのバージョンでコンパイルできたコードは、将来の1.yバージョンでも問題なくコンパイルできる」という約束をしました。

しかし、言語の進化には破壊的変更が必要な場合もあります。Editionはこの問題を解決します。

参考:doc.rust-lang.org

Edition 2024は、多くの小さな改善の集合体です。大きな単一機能ではなく、言語全体の洗練を目指しています。言語は停滞せず、かつ急激な変化も避けるという健全な進化を示しています。

参考:bertptrs.nl

Edition 2024の主要な変更点

1. Async Closures の段階的な導入

これは非同期プログラミングにおける重要な機能の1つです。Edition 2024では基本的なasync closuresがサポートされました。ただし、async Fn()トレイト構文は2025年1月時点でまだunstableです。実際にはFn() -> impl Futureの形式で記述する必要があります。

use std::time::Duration;

// ついに可能になった!
let async_closure = async || {
    tokio::time::sleep(Duration::from_secs(1)).await;
    "完了".to_string()
};

// AsyncFnトレイトを使った高階関数
// 注:async Fn()構文はまだunstableのため、以下のように記述します
async fn process_with_async_closure<F, Fut>(f: F) -> String
where
    F: Fn() -> Fut,
    Fut: std::future::Future<Output = String>,
{
    f().await
}

#[tokio::main]
async fn main() {
    let result = async_closure().await;
    println!("結果: {}", result);
    
    let result = process_with_async_closure(async || {
        "非同期クロージャ".to_string()
    }).await;
    println!("結果: {}", result);
}

これまでは、非同期のクロージャを書くために複雑な回避策が必要だったが、Edition 2024ではネイティブにサポートされるようになった。これにより、高階関数を使った非同期プログラミングが大幅に簡潔になる。

参考:medium.com

2. async fn in traits の完全サポート

これはRust 1.75.0で安定化された機能だが、Edition 2024の文脈で完全に統合された。

use std::time::Duration;

// これがついに標準機能に!
trait AsyncService {
    async fn process(&self, data: String) -> Result<String, Box<dyn std::error::Error>>;
    async fn validate(&self, input: &str) -> bool;
}

struct MyService;

impl AsyncService for MyService {
    async fn process(&self, data: String) -> Result<String, Box<dyn std::error::Error>> {
        tokio::time::sleep(Duration::from_secs(1)).await;
        Ok(format!("処理完了: {}", data))
    }
    
    async fn validate(&self, input: &str) -> bool {
        !input.is_empty()
    }
}

// ジェネリックな非同期関数でも使える
async fn use_service<T: AsyncService>(service: &T) {
    match service.process("データ".to_string()).await {
        Ok(result) => println!("{}", result),
        Err(e) => eprintln!("エラー: {}", e),
    }
}

この機能により、トレイトベースの抽象化が非同期コードでも自然に使えるようになった。Niko Matsakisは2024年の初めに「async fn in traitsはAsync Rustのハードモードを終わらせる基盤」と述べている。

参考:smallcultfollowing.com

3. PreludeへのFutureとIntoFutureの追加

// もうuseステートメントが不要!
// use std::future::Future; // ← 不要になった

async fn my_future() -> i32 {
    42
}

// FutureもIntoFutureもpreludeに含まれているので
// そのまま使える
fn process_future<F: Future<Output = i32>>(future: F) {
    // ...
}

これは小さな変更に見えるが、非同期コードを書く際の摩擦を大幅に減らす。

4. RPIT(Return Position impl Trait)のライフタイム捕捉ルールの改善

// Edition 2021での問題
fn old_way(x: &str) -> impl Future<Output = String> {
    async move {
        // xのライフタイムが正しく捕捉されない場合があった
        x.to_string()
    }
}

// Edition 2024での改善
fn new_way(x: &str) -> impl Future<Output = String> {
    async move {
        // ライフタイムが適切に捕捉される
        x.to_string()
    }
}

// use<..> で明示的な制御も可能
fn explicit_capture<'a>(x: &'a str) -> impl Future<Output = String> + use<'a> {
    async move {
        x.to_string()
    }
}

この改善により、非同期関数から返されるimpl Future型のライフタイム推論がより直感的になった。

参考:www.heise.de

5. 一時変数のスコープ改善

// if let での一時変数のドロップタイミングが改善
async fn process() {
    if let Some(data) = fetch_data().await {
        // Edition 2024では、dataはこのブロック内でのみ有効
        println!("{}", data);
    } // ← dataはここでドロップされる
}

// tail expressionでの改善
async fn compute() -> i32 {
    let result = calculate().await;
    result * 2  // この一時変数のスコープも改善された
}

これは非同期コードにおける「一時的な値がスコープ外になるまで保持される」問題を解決します。以前はコンパイルエラーになっていたコードが、Edition 2024では正しく動作します。

6. unsafeの厳格化

// Edition 2024では、extern blockはunsafeマーク必須
unsafe extern "C" {
    fn external_function();
}

// 環境変数の操作もunsafeに
unsafe {
    std::env::set_var("KEY", "value");
}

unsafeの範囲がより明確になり、安全でない操作がコード中で目立つようになった。これにより、コードレビュー時に安全性を検証しやすくなる。

Edition 2024への移行

[package]
name = "my-async-app"
version = "0.1.0"
edition = "2024"  # ← ここを変更

[dependencies]
tokio = { version = "1", features = ["full"] }

多くの場合、cargo fixで自動的に移行できる:

cargo fix --edition

実践例:Edition 2024の機能を使ったコード

use std::time::Duration;

// Async closureを使った例
async fn process_items<F, Fut>(items: Vec<String>, processor: F) -> Vec<String>
where
    F: Fn(String) -> Fut,
    Fut: std::future::Future<Output = String>,
{
    let mut results = Vec::new();
    for item in items {
        results.push(processor(item).await);
    }
    results
}

// Async trait methodを使った例
trait DataProcessor {
    async fn process(&self, data: &str) -> String;
}

struct UppercaseProcessor;

impl DataProcessor for UppercaseProcessor {
    async fn process(&self, data: &str) -> String {
        tokio::time::sleep(Duration::from_millis(100)).await;
        data.to_uppercase()
    }
}

#[tokio::main]
async fn main() {
    // Async closureの使用
    let items = vec!["hello".to_string(), "world".to_string()];
    let results = process_items(items, |item| async move {
        format!("処理済み: {}", item)
    })
    .await;
    
    println!("結果: {:?}", results);
    
    // Async traitの使用
    let processor = UppercaseProcessor;
    let result = processor.process("rust 2024").await;
    println!("変換結果: {}", result);
}

非同期Rustのベストプラクティス(2024-2025)

1. ブロッキング操作を避ける

// ❌ 悪い例
#[tokio::main]
async fn main() {
    tokio::spawn(async {
        // std::thread::sleepはスレッドをブロックする
        std::thread::sleep(Duration::from_secs(5));
    });
}

// ✅ 良い例
#[tokio::main]
async fn main() {
    tokio::spawn(async {
        // tokio::time::sleepは非同期
        tokio::time::sleep(Duration::from_secs(5)).await;
    });
}

Tokioのような非同期ランタイムでは、ブロッキング操作は他のタスクの実行を妨げる。常に非同期版の関数を使用すること。

参考:blog.poespas.me

2. CPU集約的な処理は別スレッドで

use tokio::task;

fn cpu_intensive_work(n: u64) -> u64 {
    // フィボナッチ数の計算など
    (0..n).sum()
}

#[tokio::main]
async fn main() {
    // CPU集約的な処理はspawn_blockingで
    let result = task::spawn_blocking(|| {
        cpu_intensive_work(1_000_000)
    }).await.unwrap();
    
    println!("結果: {}", result);
}

非同期ランタイムはI/O待機に最適化されている。CPU集約的なタスクはspawn_blockingを使って別スレッドプールで実行します。

参考:medium.com

3. 適切なランタイム設定

// シングルスレッドランタイム(軽量)
#[tokio::main(flavor = "current_thread")]
async fn main() {
    // 単純なI/O処理に適している
}

// マルチスレッドランタイム(デフォルト)
#[tokio::main(flavor = "multi_thread", worker_threads = 4)]
async fn main() {
    // 多数の並行タスクがある場合
}

アプリケーションの特性に応じてランタイムを選択します。

4. エラーの適切な伝搬

use anyhow::Result;

async fn step1() -> Result<String> {
    Ok("ステップ1完了".to_string())
}

async fn step2() -> Result<String> {
    Ok("ステップ2完了".to_string())
}

async fn process() -> Result<()> {
    let result1 = step1().await?;
    let result2 = step2().await?;
    
    println!("{}", result1);
    println!("{}", result2);
    
    Ok(())
}

#[tokio::main]
async fn main() {
    if let Err(e) = process().await {
        eprintln!("エラー: {}", e);
    }
}

?演算子を活用し、エラーを適切に伝搬させる。anyhowクレートは便利なエラーハンドリングを提供します。

5. Send境界の理解

use std::sync::Mutex;
use tokio::sync::Mutex as TokioMutex;

// ❌ 悪い例:std::sync::MutexGuardはSendではない
async fn bad_example() {
    let data = Mutex::new(0);
    let guard = data.lock().unwrap();
    
    // これはコンパイルエラー
    // some_async_function().await;
}

// ✅ 良い例:tokio::sync::Mutexを使用
async fn good_example() {
    let data = TokioMutex::new(0);
    let guard = data.lock().await;
    
    // これは問題ない
    some_async_function().await;
}

マルチスレッドランタイムでは、awaitポイントを跨ぐデータはSendでなければならない。tokio::syncの型を使用すること。

参考:www.shuttle.dev


実践:非同期HTTPリクエストで性能を比較

実際のコードで、非同期の威力を確認してみよう。

依存関係の設定

[dependencies]
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.11", features = ["json"] }

同期的なアプローチ

use std::time::Instant;
use reqwest::Error;

#[tokio::main]
async fn main() -> Result<(), Error> {
    let url = "https://3-shake.com/";
    let start_time = Instant::now();

    // 順番に4回リクエスト
    for i in 1..=4 {
        let response = reqwest::get(url).await?;
        println!("リクエスト {} 完了: ステータス {}", i, response.status());
    }

    let elapsed = start_time.elapsed();
    println!("合計時間: {} ms", elapsed.as_millis());
    // 出力例: 合計時間は環境により変動(概ね数百ms程度)

    Ok(())
}

非同期的なアプローチ

use std::time::Instant;
use reqwest::Error;

#[tokio::main]
async fn main() -> Result<(), Error> {
    let url = "https://3-shake.com/";
    let start_time = Instant::now();

    // 4つのリクエストを同時に実行
    let (r1, r2, r3, r4) = tokio::join!(
        reqwest::get(url),
        reqwest::get(url),
        reqwest::get(url),
        reqwest::get(url),
    );

    // 結果を確認
    println!("リクエスト1: {:?}", r1.map(|r| r.status()));
    println!("リクエスト2: {:?}", r2.map(|r| r.status()));
    println!("リクエスト3: {:?}", r3.map(|r| r.status()));
    println!("リクエスト4: {:?}", r4.map(|r| r.status()));
    
    let elapsed = start_time.elapsed();
    println!("合計時間: {} ms", elapsed.as_millis());
    // 出力例: 合計時間は環境により変動

    Ok(())
}

並行実行による性能改善

ネットワークのレスポンスを待っている間、CPUは他のリクエストを処理できます。実際の性能改善はネットワーク状況、サーバーの同時接続制限、HTTP/2の利用状況などに依存しますが、理想的な条件下では、並行実行により順次実行と比べて大幅な時間短縮が可能です。


より実践的な例:並行データ処理

複数のAPIからデータを取得して統合する、よくあるシナリオを見てみましょう。

use reqwest::Error;
use serde::Deserialize;
use std::time::Instant;

#[derive(Deserialize, Debug)]
struct User {
    id: i32,
    name: String,
    email: String,
}

#[derive(Deserialize, Debug)]
struct Post {
    id: i32,
    title: String,
    body: String,
}

#[derive(Debug)]
struct UserProfile {
    user: User,
    posts: Vec<Post>,
    comments_count: usize,
}

async fn fetch_user(user_id: i32) -> Result<User, Error> {
    let url = format!(
        "https://jsonplaceholder.typicode.com/users/{}", 
        user_id
    );
    reqwest::get(&url)
        .await?
        .json::<User>()
        .await
}

async fn fetch_user_posts(user_id: i32) -> Result<Vec<Post>, Error> {
    let url = format!(
        "https://jsonplaceholder.typicode.com/posts?userId={}", 
        user_id
    );
    reqwest::get(&url)
        .await?
        .json::<Vec<Post>>()
        .await
}

async fn fetch_comments_count(user_id: i32) -> Result<usize, Error> {
    let url = format!(
        "https://jsonplaceholder.typicode.com/comments?postId={}", 
        user_id
    );
    let comments = reqwest::get(&url)
        .await?
        .json::<Vec<serde_json::Value>>()
        .await?;
    Ok(comments.len())
}

async fn get_user_profile(user_id: i32) -> Result<UserProfile, Error> {
    // 3つのAPIを並行して呼び出す
    let (user_result, posts_result, comments_result) = tokio::join!(
        fetch_user(user_id),
        fetch_user_posts(user_id),
        fetch_comments_count(user_id),
    );

    Ok(UserProfile {
        user: user_result?,
        posts: posts_result?,
        comments_count: comments_result?,
    })
}

#[tokio::main]
async fn main() -> Result<(), Error> {
    let start = Instant::now();
    
    let profile = get_user_profile(1).await?;
    
    let duration = start.elapsed();
    
    println!("ユーザー: {}", profile.user.name);
    println!("投稿数: {}", profile.posts.len());
    println!("コメント数: {}", profile.comments_count);
    println!("\n処理時間: {} ms", duration.as_millis());
    
    Ok(())
}

3つのAPI呼び出しを並行実行することで、順次実行する場合の3分の1程度の時間で完了します。


Future とタスクの理解

Rustの非同期処理の核心は Future トレイトです。

pub trait Future {
    type Output;
    
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) 
        -> Poll<Self::Output>;
}

pub enum Poll<T> {
    Ready(T),
    Pending,
}

async関数は、Futureを返す関数に変換されます。

// これを書くと...
async fn example() -> i32 {
    42
}

// コンパイラがこのように変換する
fn example() -> impl Future<Output = i32> {
    // 状態機械の実装
}

Futureは遅延評価されます。awaitされるまで実行されない。これにより、効率的なリソース管理が可能になる。

参考:cosmicmeta.io

カスタムFutureの実装

理解を深めるため、カウンターFutureを自作する例を示します。

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::Duration;

struct CounterFuture {
    count: u32,
    max: u32,
}

impl CounterFuture {
    fn new(max: u32) -> Self {
        Self { count: 0, max }
    }
}

impl Future for CounterFuture {
    type Output = u32;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) 
        -> Poll<Self::Output> 
    {
        self.count += 1;
        println!("ポーリング #{}: カウント = {}", self.count, self.count);
        
        // 実際の待機をシミュレート
        std::thread::sleep(Duration::from_millis(100));
        
        if self.count < self.max {
            // まだ完了していない
            cx.waker().wake_by_ref();
            Poll::Pending
        } else {
            // 完了
            Poll::Ready(self.count)
        }
    }
}

#[tokio::main]
async fn main() {
    let future1 = CounterFuture::new(3);
    let future2 = CounterFuture::new(3);
    
    let (result1, result2) = tokio::join!(future1, future2);
    
    println!("\n結果1: {}", result1);
    println!("結果2: {}", result2);
}

出力:

ポーリング #1: カウント = 1
ポーリング #1: カウント = 1
ポーリング #2: カウント = 2
ポーリング #2: カウント = 2
ポーリング #3: カウント = 3
ポーリング #3: カウント = 3

結果1: 3
結果2: 3

2つのFutureが交互にポーリングされている様子がわかる。


エラーハンドリングとキャンセル安全性

エラーハンドリングのベストプラクティス

use std::time::Duration;
use tokio::time::timeout;

async fn risky_operation() -> Result<String, &'static str> {
    tokio::time::sleep(Duration::from_secs(2)).await;
    Ok("成功".to_string())
}

async fn slow_operation() -> Result<String, &'static str> {
    tokio::time::sleep(Duration::from_secs(10)).await;
    Ok("遅い操作完了".to_string())
}

#[tokio::main]
async fn main() {
    // タイムアウト付き実行
    let result = timeout(
        Duration::from_secs(3),
        slow_operation()
    ).await;
    
    match result {
        Ok(Ok(value)) => println!("成功: {}", value),
        Ok(Err(e)) => println!("操作エラー: {}", e),
        Err(_) => println!("タイムアウト"),
    }
    
    // 複数の操作を並行実行し、エラーを適切に処理
    let results = tokio::join!(
        risky_operation(),
        risky_operation(),
        risky_operation(),
    );
    
    match results {
        (Ok(r1), Ok(r2), Ok(r3)) => {
            println!("すべて成功: {}, {}, {}", r1, r2, r3);
        }
        _ => {
            println!("一部が失敗しました");
        }
    }
}

キャンセル安全性

Tyler Mandryは「Making Async Rust Reliable」で、キャンセル安全性の重要性を強調している。

参考:tmandry.gitlab.io

use tokio::sync::Mutex;
use std::sync::Arc;

// キャンセル安全でない例
async fn unsafe_increment(counter: Arc<Mutex<i32>>) {
    let mut guard = counter.lock().await;
    *guard += 1;
    // ここでキャンセルされると、ロックが保持されたまま
    tokio::time::sleep(Duration::from_secs(1)).await;
}

// キャンセル安全な例
async fn safe_increment(counter: Arc<Mutex<i32>>) {
    let mut guard = counter.lock().await;
    *guard += 1;
    drop(guard); // 明示的にロックを解放
    
    tokio::time::sleep(Duration::from_secs(1)).await;
}

実践的なパターン集

パターン1: 並行実行で最初に完了したものを使う

async fn fetch_from_server_a() -> Result<String, Box<dyn std::error::Error>> {
    tokio::time::sleep(Duration::from_secs(2)).await;
    Ok("サーバーA".to_string())
}

async fn fetch_from_server_b() -> Result<String, Box<dyn std::error::Error>> {
    tokio::time::sleep(Duration::from_secs(1)).await;
    Ok("サーバーB".to_string())
}

#[tokio::main]
async fn main() {
    use tokio::select;
    
    // 最初に完了したほうを使う
    select! {
        result_a = fetch_from_server_a() => {
            println!("サーバーAから: {:?}", result_a);
        }
        result_b = fetch_from_server_b() => {
            println!("サーバーBから: {:?}", result_b);
        }
    }
}

パターン2: 複数のタスクをスポーンして管理

use tokio::task::JoinHandle;

async fn worker(id: i32, duration: u64) -> String {
    tokio::time::sleep(Duration::from_secs(duration)).await;
    format!("ワーカー {} 完了", id)
}

#[tokio::main]
async fn main() {
    let mut handles: Vec<JoinHandle<String>> = Vec::new();
    
    // 複数のワーカーをスポーン
    for i in 0..5 {
        let handle = tokio::spawn(worker(i, i as u64 + 1));
        handles.push(handle);
    }
    
    // すべての完了を待つ
    for handle in handles {
        match handle.await {
            Ok(result) => println!("{}", result),
            Err(e) => eprintln!("エラー: {}", e),
        }
    }
}

パターン3: 共有状態の安全な管理

use tokio::sync::RwLock;
use std::sync::Arc;

#[derive(Clone)]
struct Counter {
    value: Arc<RwLock<i32>>,
}

impl Counter {
    fn new() -> Self {
        Self {
            value: Arc::new(RwLock::new(0)),
        }
    }
    
    async fn increment(&self) {
        let mut value = self.value.write().await;
        *value += 1;
    }
    
    async fn get(&self) -> i32 {
        let value = self.value.read().await;
        *value
    }
}

#[tokio::main]
async fn main() {
    let counter = Counter::new();
    let mut handles = vec![];
    
    // 10個のタスクで並行してインクリメント
    for _ in 0..10 {
        let counter_clone = counter.clone();
        let handle = tokio::spawn(async move {
            for _ in 0..100 {
                counter_clone.increment().await;
            }
        });
        handles.push(handle);
    }
    
    // すべて完了を待つ
    for handle in handles {
        handle.await.unwrap();
    }
    
    println!("最終カウント: {}", counter.get().await);
    // 出力: 最終カウント: 1000
}

まとめ

非同期処理における重要な概念を再確認しましょう。

  • 並行性(Concurrency): 複数のタスクを論理的に同時進行させる技術
  • 効率性(Efficiency): 待ち時間を無駄にせず他のタスクを処理すること
  • スケーラビリティ(Scalability): リソースを効果的に使い多数のタスクを処理する能力
  • 応答性(Responsiveness): ユーザーを待たせず素早く反応すること

Rustの非同期処理は、型システムによる安全性保証と高い実行性能を両立しています。async/await構文はコードを簡潔に保ち、Futureトレイトは強力な抽象化を提供します。Tokioなどのランタイムは成熟しており、本番環境での使用実績も豊富です。

Edition 2024以降も、Rustの非同期エコシステムは進化を続けています。async closures、send bound problem、async generatorsなど、さらなる改善が予定されています。

参考:www.javacodegeeks.com

非同期処理の本質

非同期処理の本質は、待ち時間を効率的に活用することです。サーバからのレスポンスを待ちながら他のリクエストを処理し、ファイルの読み込みを待ちながら計算を行います。一つのタスクの完了を待たずに次のタスクを開始することで、限られたリソースを最大限に活用できます。

Rustの非同期処理の特徴

Rustの非同期処理は、型システムによる安全性保証と高い実行性能を両立しています。async/await構文はコードを簡潔に保ち、Futureトレイトは強力な抽象化を提供します。Edition 2024では、async closuresやasync fn in traitsのサポートが進み、より表現力の高い非同期コードが書けるようになりました。

学習のポイント

初学者にとって、SendSyncのトレイト境界やライフタイムの扱いは困難に感じるかもしれません。しかし、これらの制約は、並行処理における安全性を保証するために必要なものです。Rustコンパイラのエラーメッセージは、タスクの依存関係やリソースの所有権について正しく考えるための指針となります。

Edition 2024における改善は、単なる文法の追加ではありません。「複数の時間軸を同時に扱う」という非同期処理の考え方が、言語の中により深く統合されたことを意味したのかなぁって思います。

おわり


参考リンク

公式ドキュメント

開発ロードマップ

コミュニティリソース




以上の内容はhttps://syu-m-5151.hatenablog.com/entry/2025/11/18/155416より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14