Rustのasync fn in traits - 安定化した機能と残る制約

上級 | 8分 で読める | 2026.04.19

公式ドキュメント

最初に時系列を訂正する

Rustのtrait内async fnはRust 1.90で安定した機能ではありません。Rust 1.75で、return-position impl Trait in traitsとともに安定しました。Rust 1.75の公開日は2023年12月28日です。

2025年2月のRust 1.85では、Rust 2024 Editionとasync closureが安定しました。これらはasync Rustを扱いやすくしますが、runtimeやI/O driverをstandard libraryへ統合するものではありません。

この記事では、市場予測や言語間の固定性能値を使わず、安定化した機能と残る制約を確認します。

traitにasync fnを書く

Rust 1.75以降では、traitへ直接async fnを定義できます。

trait UserRepository {
    async fn find_name(&self, id: u64) -> Result<String, RepoError>;
}

struct DatabaseRepository;

impl UserRepository for DatabaseRepository {
    async fn find_name(&self, id: u64) -> Result<String, RepoError> {
        let name = load_name_from_database(id).await?;
        Ok(name)
    }
}

async fnは、概念的にはFutureを返すfunctionのsyntax sugarです。

trait UserRepository {
    fn find_name(
        &self,
        id: u64,
    ) -> impl Future<Output = Result<String, RepoError>>;
}

実際のdesugarにはlifetimeなども関係しますが、戻り値の具体的なFuture型を直接書かずに済む点が重要です。

public traitとSend bound

公式発表は、public traitでasync fnを使うときの制約を説明しています。traitを利用する側が、返されるFutureへ後からSend boundを追加できないためです。

multi-thread runtimeでtaskをspawnするAPIは、FutureへSend + 'staticを求めることがあります。libraryのpublic APIでは、利用環境に必要なboundを先に設計します。

Rust公式のtrait_variant crateを使うと、local用traitとSendを要求するvariantを生成できます。

#[trait_variant::make(HttpService: Send)]
pub trait LocalHttpService {
    async fn fetch(&self, url: String) -> Result<String, HttpError>;
}

生成されるHttpService側では、implementorと返されるFutureにSendが要求されます。WebAssemblyやsingle-thread executorではlocal variantが適する場合があります。

dyn Traitの制約

traitにasync fnを書けることと、dyn Traitとして使えることは同じではありません。

// 用途によっては、この形をそのまま利用できない
let service: Box<dyn HttpService>;

return-position impl Traitを含むtraitは、通常のobject-safe traitと同じ方法ではdynamic dispatchできません。

dynamic dispatchが必要なら、次を検討します。

  • enumで実装候補を表す
  • generic parameterでstatic dispatchする
  • async-trait crateでboxed Futureへ変換する
  • 対応する補助crateを評価する

boxed Futureはallocationやindirectionを伴う可能性がありますが、pluginのように実装型をruntimeで切り替えたい場合に役立ちます。性能だけでなくAPIの単純さと必要な柔軟性で選びます。

async closure

Rust 1.85ではasync closureが安定しました。

async fn retry<F, Fut, T, E>(mut operation: F) -> Result<T, E>
where
    F: FnMut() -> Fut,
    Fut: Future<Output = Result<T, E>>,
{
    operation().await
}

let result = retry(async || {
    fetch_current_user().await
}).await;

async closureは周囲の値をborrowでき、非同期callbackを表しやすくします。実際のAPIでは、呼び出し回数、borrowの期間、Send'staticが必要かを確認します。

runtimeは別に選ぶ

Rust standard libraryはFuture traitやasync/await構文を提供しますが、network I/O、timer、task schedulerを含む汎用async runtimeは提供しません。

代表的な選択肢にはTokioやasync-std、smolなどがあります。ecosystemではTokioを前提とするcrateも多いですが、用途ごとに確認します。

確認項目内容
runtime互換利用crateがどのruntimeを前提にするか
thread modelmulti-threadかcurrent-threadか
cancellationFutureをdropしたときのresource処理
timeout外部I/Oへ期限を設定できるか
blocking処理async task上で直接実行していないか
observabilitytracing、metrics、task調査

runtime versionの数字だけで性能を判断せず、自分が使うI/Oとconcurrencyでtestします。

Tokioでの最小例

use tokio::time::{timeout, Duration};

#[tokio::main]
async fn main() {
    let result = timeout(
        Duration::from_secs(2),
        fetch_message(),
    )
    .await;

    match result {
        Ok(Ok(message)) => println!("{message}"),
        Ok(Err(error)) => eprintln!("request failed: {error}"),
        Err(_) => eprintln!("request timed out"),
    }
}

timeoutで外側のFutureを中断しても、接続先やlibraryが持つresourceの後処理まで期待通りか確認します。

CPUを長時間使う同期処理をasync taskで直接実行すると、executorのworker threadを塞ぎます。Tokioではspawn_blockingを使う方法がありますが、同時実行数とshutdown時の挙動も設計します。

cancellation safety

select!やtimeoutによってFutureが途中でdropされるとき、操作がどこまで進んだかを考える必要があります。

tokio::select! {
    result = receive_message() => {
        handle(result?);
    }
    _ = shutdown_signal() => {
        save_state().await?;
    }
}

途中まで読み取ったdata、送信中のmessage、transaction、lock guardがどうなるかを確認します。再試行可能な操作か、idempotency keyが必要かも判断します。

errorとtaskを追跡する

非同期処理では、taskをspawnしたままJoinHandleを捨てると、失敗を見落とすことがあります。

let handle = tokio::spawn(async {
    process_queue().await
});

match handle.await {
    Ok(Ok(())) => {}
    Ok(Err(error)) => eprintln!("task failed: {error}"),
    Err(join_error) => eprintln!("task panicked: {join_error}"),
}

productionではstructured loggingやtracingを使い、request ID、task名、処理時間、error chainを追えるようにします。並行taskが増えるほど、単純なprintln!だけでは因果関係を追いにくくなります。

Rust for Linuxとの関係

Linux kernelへRust supportがmergeされたのはLinux 6.1です。これはasync traitの安定化やTokioの採用を意味しません。

kernel codeは通常のuserspace Web serviceと実行環境が異なり、利用できるRust機能やcrateにも制約があります。Rust for LinuxとWeb frameworkやasync runtimeは、別の技術領域として確認します。

導入チェックリスト

  • 必要なMSRVを決めた
  • public traitのFutureにSendが必要か決めた
  • dynamic dispatchの必要性を確認した
  • runtimeを重複して持ち込んでいない
  • blocking処理を分離した
  • timeoutとcancellationをtestした
  • spawned taskの失敗を回収している
  • productionに近い負荷で測定した

まとめ

  • trait内async fnはRust 1.75で安定した
  • Rust 1.85ではasync closureとRust 2024 Editionが安定した
  • public traitではSend boundを先に設計する
  • async fn in traitは自動的にdyn Trait対応にはならない
  • async runtime、cancellation、blocking処理は別途設計する
  • Linux kernelのRust supportとuserspace async ecosystemを混同しない

参考リソース

← 一覧に戻る
PR
PR
PR
PR