最初に時系列を訂正する
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-traitcrateで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 model | multi-threadかcurrent-threadか |
| cancellation | Futureをdropしたときのresource処理 |
| timeout | 外部I/Oへ期限を設定できるか |
| blocking処理 | async task上で直接実行していないか |
| observability | tracing、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では
Sendboundを先に設計する async fn in traitは自動的にdyn Trait対応にはならない- async runtime、cancellation、blocking処理は別途設計する
- Linux kernelのRust supportとuserspace async ecosystemを混同しない
参考リソース
- Announcing async fn and RPIT in traits
- Announcing Rust 1.75.0
- Announcing Rust 1.85.0 and Rust 2024
- The Async Book
- Rust for Linux documentation