Rustのasync/awaitは、I/O待ちの多い処理を同期コードに近い見た目で書くための構文です。ただしJavaScriptやPythonと違い、Rustの標準ライブラリには非同期タスクを動かすランタイムが入っていません。async fnを呼んだだけでは何も実行されず、Tokioなどのランタイムに渡して初めて動きます。構文でFutureを作ることと、ランタイム(executor)でFutureを進めることを分けて理解すると、呼び出しても処理が始まらない理由を整理できます。
本記事の掲載コードとエラーメッセージは、2026年10月25日時点の安定版Rust 1.99.1(2026年10月15日公開)とTokio 1.54.1(edition 2024)で実際にコンパイル・実行して取得したものです。未安定の機能はnightly(rustc 1.101.0-nightly、2026年10月23日ビルド)で動かしました。所有権や借用の基礎から確認したい場合は、先にRust入門|環境構築から所有権・基本文法、受託開発での採用判断までを読んでおくと以降の説明が追いやすくなります。
まとめ:Rustのasync/awaitの要点
async fnは本体を実行せず、Futureを返すだけです。.awaitするかランタイムに渡すまで1行も動きません。JavaScriptのPromiseが作った時点で動き出すのとは逆です。- async/awaitはRust 1.39.0(2019年11月7日)で安定化しました。ランタイムは標準に含まれず、実務ではTokio(2026年10月時点で1.54.1)が事実上の標準です。async-stdは2025年3月に開発終了を告知しており、新規採用は避けてください。
tokio::join!は1つのタスクの中で待ちを重ね、tokio::spawnは別タスクとして登録するので、マルチスレッドのランタイムでは並列に動く余地があります。CPU処理2本の実測では、spawnがjoin!のほぼ半分の時間で終わりました。- トレイトの
async fnはRust 1.75.0で標準化されましたが、dyn Traitにはできず、トレイトの宣言だけでは返すFutureがSendであることも保証されません。dynが要るならasync-trait、Sendが要るなら-> impl Future + Sendの宣言かtrait-variantを使います。 - 非同期イテレータ(async gen)・AsyncDrop・
yieldを書けるコルーチンは、Rust 1.99.1でもnightly限定です。安定版ではfuturesクレートのStreamと、明示的なasync fn closeで代用します。
async fnと.awaitの基本構文
async fn・asyncブロック・.awaitの書き方
関数の前にasyncを付けると、戻り値がimpl Future<Output = 戻り値の型>に変わります。呼び出し側は.awaitを後置して完了を待ちます。.awaitはasync fn、asyncブロック、非同期クロージャの中で書けます。
次の例は、200ミリ秒かかる処理を「順番に待つ」場合と「同時に待つ」場合を並べたものです。依存関係の追加はcargo add [email protected] --features fullで行いました。以降の例で使うfutures・async-trait・trait-variantはcargo add [email protected] [email protected] [email protected]で追加します(Cargoの操作はCargo(Rust)とは?インストールと使い方・主要コマンド一覧を参照)。
use std::time::Duration;
use tokio::time::sleep;
async fn fetch_user(id: u32) -> String {
sleep(Duration::from_millis(200)).await; // I/O待ちの代わり
format!("user{id}")
}
#[tokio::main]
async fn main() {
let a = fetch_user(1).await; // 順番に待つ
let b = fetch_user(2).await;
println!("{a} {b}");
let (c, d) = tokio::join!(fetch_user(3), fetch_user(4)); // 同時に待つ
println!("{c} {d}");
}
user1 user2
user3 user4
.awaitを2回続けると、1つ目が終わるまで2つ目は始まりません。tokio::join!は渡したFutureを同じタスク内で交互に進めるため、待ち時間が重なって合計がほぼ1回分に縮みます。ここで起きているのは並行(concurrent)であって、スレッドを増やす並列(parallel)ではありません。
.awaitを付け忘れたasync fnが実行されない理由
RustのFutureは「遅延評価」です。呼び出した時点では中身の処理を始めず、誰かが進めるまで何も起きません。.awaitを付け忘れるとコンパイラは警告を出しますが、エラーにはならないので見落としやすい箇所です。
async fn greet() {
println!("greet が実行された");
}
#[tokio::main]
async fn main() {
greet(); // .await を付け忘れた
println!("main の終わり");
}
warning: unused implementer of `Future` that must be used
--> src/bin/lazy.rs:7:5
|
7 | greet(); // .await を付け忘れた
| ^^^^^^^
|
= note: futures do nothing unless you `.await` or poll them
= note: `#[warn(unused_must_use)]` (part of `#[warn(unused)]`) on by default
main の終わり
出力に「greet が実行された」は現れません。警告文のfutures do nothing unless you `.await` or poll themがそのまま答えで、Futureは.awaitされるかpollされるまで何もしません。
JavaScriptのPromiseとRustのFutureの違い
同じ形のコードをJavaScript(Node.js v26.5.0)で実行すると、出力の順番が逆になります。
async function greet() {
console.log("greet が実行された");
}
greet(); // await を付けない
console.log("main の終わり");
greet が実行された
main の終わり
JavaScriptのasync functionは、呼んだ時点で最初のawaitまで本体が進みます。MDNのPromiseコンストラクターの解説も、executorは「同期的に(Promise が構築されるとすぐに)呼び出されます」と書いています。実行役のイベントループも処理系に内蔵されているため、ランタイムを選ぶ工程がありません(Node.js側の仕組みはNode.jsとは?仕組み・版の選び方・標準機能で動かす手順を実装目線で解説を参照)。
もう1つの違いはキャンセルです。RustのFutureは、dropされるとそのFuture自体は以後pollされず、処理がその場で止まります。ただしtokio::spawnで別タスクにした処理は、JoinHandleをdropしても止まらず裏で動き続けます。次の例では100ミリ秒で打ち切ったあと600ミリ秒待っていますが、「job: 終了」は最後まで出力されません。
use std::time::Duration;
async fn job() {
println!("job: 開始");
tokio::time::sleep(Duration::from_millis(500)).await;
println!("job: 終了"); // ここまで進まない
}
#[tokio::main]
async fn main() {
let fut = job();
let r = tokio::time::timeout(Duration::from_millis(100), fut).await;
println!("timeout: {}", r.is_err());
tokio::time::sleep(Duration::from_millis(600)).await;
println!("main の終わり");
}
job: 開始
timeout: true
main の終わり
| 観点 | JavaScriptのPromise | RustのFuture |
|---|---|---|
| 処理の開始 | 生成した時点 | .awaitかpollされた時点 |
| 実行する仕組み | 処理系内蔵のイベントループ | Tokio等のランタイムを追加 |
| 途中で止める | 標準の手段なし(AbortController等を別に渡す) | Future本体をdrop(spawn済みはabort) |
| awaitの位置 | 前置(await p) | 後置(f.await) |
Futureトレイトとpollの仕組み
Futureトレイトの定義とPoll::Pending
std::future::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,
}
ランタイム(executor)はpollを呼び、結果がPoll::Ready(値)なら完了、Poll::Pendingなら「まだ終わっていない」と受け取ります。Pendingを返すFutureは、再開できる状態になったらcx.waker()で起こしてもらうよう登録しておく約束です。これを守らないFutureは、再度pollされる機会を得られず、処理が停止したままになるおそれがあります。
手でFutureを実装すると、この往復が見えます。3回Pendingを返してから完了するFutureを、futuresクレートの最小executorであるfutures::executor::block_onで動かした例です。
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
struct CountDown(u32);
impl Future for CountDown {
type Output = &'static str;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if self.0 == 0 {
Poll::Ready("完了")
} else {
println!("poll: 残り{}", self.0);
self.0 -= 1;
cx.waker().wake_by_ref(); // もう一度 poll してほしいと executor に伝える
Poll::Pending
}
}
}
fn main() {
let result = futures::executor::block_on(CountDown(3));
println!("{result}");
}
poll: 残り3
poll: 残り2
poll: 残り1
完了
wake_by_ref()の行を消すと、block_onは1回目のPendingのあと起床通知を待ち続け、プログラムは終了しません。1回だけ小さなFutureを書きたいときは、Rust 1.64.0で安定化したstd::future::poll_fnを使えば構造体を定義せずに済みます。
コンパイラが生成する状態マシンとコルーチン
async fnは、コンパイラの内部で「コルーチン」に変換されます。RustのUnstable Bookのcoroutines項は、コルーチンの現在の主な用途を「async/awaitとgen構文の実装プリミティブ」と説明し、コンパイラ内では状態マシンとして生成されると書いています。.awaitの位置ごとに状態が1つでき、その時点で生きている変数だけが状態の中に保存されます。
この仕組みのおかげで、Rustのasyncはタスクごとにスタックを確保するグリーンスレッド型と違い、Futureの大きさがコンパイル時に決まります。OSスレッドやグリーンスレッドとの対比はグリーンスレッドとは?OSスレッドとの違いとN:1モデルの制約を実測で解説で扱っています。yieldを書けるコルーチン構文そのもの(#![feature(coroutines)])はnightly限定で、後半の「nightlyで動かすコルーチン・async gen・AsyncDrop」の章で実際に動かした結果を載せています。
自己参照を持つFutureとPinによる固定
状態マシンは、自分自身のフィールドを指す参照を持つことがあります。.awaitをまたいでlet s = String::new(); let r = &s;のような借用を保つと、Futureの中に「自分の一部への参照」が入るためです。この値がメモリ上で移動すると参照が壊れるので、pollはPin<&mut Self>を受け取り、移動しないことを型で保証しています。
実務でPinを意識するのは、Futureやストリームを変数に入れて何度もpollする場面です。次のコードはpin!を付けないとcannot be unpinned(Unpinが実装されていない)のエラーになりました。Rust 1.68.0で安定化したstd::pin::pin!を使えば、追加のヒープ確保なしでローカルに固定できます。非同期処理内では、その保存先は外側のFutureが置かれる場所によります。
use futures::stream::{self, StreamExt};
use std::pin::pin;
#[tokio::main]
async fn main() {
let mut s = pin!(stream::iter(1..=3).then(async |n| n * 10));
while let Some(v) = s.next().await {
println!("{v}");
}
}
async/awaitを動かすランタイム:Tokioの使い方
#[tokio::main]が展開されるコード
main関数はasyncにできません。#[tokio::main]は、Tokioのランタイムを組み立ててasyncな本体をblock_onで実行するコードに書き換える手続き的マクロです(マクロの仕組みはRustプロシージャルマクロの仕組みを徹底解説: コンパイル時コード生成の流れと内部処理を参照)。tokio-macrosのドキュメントが示す等価コードは次のとおりです。
fn main() {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
println!("Hello world");
})
}
既定はマルチスレッドのランタイムで、ワーカースレッド数はCPUコア数です(環境変数TOKIO_WORKER_THREADSで変更可)。#[tokio::main(flavor = "current_thread")]と書けば、呼び出し元の1スレッドだけで全タスクを回します。CLIツールやテストではcurrent_threadで足りる場合が多くあります。ただし、この設定でもtokio::spawnはSendを要求し、SendでないタスクにはLocalSetとspawn_localが必要です。
std::thread::sleepとtokio::time::sleepの違い
async関数の中でstd::thread::sleepを呼ぶと、そのタスクだけでなくスレッドごと止まります。同じ1秒待ちをcurrent_threadのランタイムで2つ同時に走らせ、所要時間を比べました。
use std::time::{Duration, Instant};
async fn std_sleep(id: u32) {
std::thread::sleep(Duration::from_secs(1)); // スレッドごと止める
println!("std {id}");
}
async fn tokio_sleep(id: u32) {
tokio::time::sleep(Duration::from_secs(1)).await; // タスクだけ止める
println!("tokio {id}");
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
let t = Instant::now();
tokio::join!(std_sleep(1), std_sleep(2));
println!("std::thread::sleep: {:.1}秒", t.elapsed().as_secs_f64());
let t = Instant::now();
tokio::join!(tokio_sleep(1), tokio_sleep(2));
println!("tokio::time::sleep: {:.1}秒", t.elapsed().as_secs_f64());
}
std 1
std 2
std::thread::sleep: 2.1秒
tokio 2
tokio 1
tokio::time::sleep: 1.0秒
std::thread::sleepでは2本が順番に実行されて2.1秒、tokio::time::sleepでは待ちが重なって1.0秒でした。ファイル読み込みや同期版のDBドライバなど、中でスレッドを止める関数はすべて同じ問題を起こします。避けられない同期処理はtokio::task::spawn_blockingで専用のスレッドプール(既定の上限512本)へ逃がします。
tokio::join!とtokio::spawnの違い:並行と並列の実測
tokio::join!は、渡したFutureを1つのタスクの中で交互にpollします。I/Oの待ち時間は重なりますが、使うスレッドは1本です。tokio::spawnはFutureを別タスクとして登録するため、マルチスレッドのランタイムでは空いているワーカースレッドで並列に実行されることがあります(どのワーカーで動くかはスケジューラが決め、current_threadでは並列になりません)。差がはっきり出るのは、.awaitを挟まないCPU処理です。
use std::time::Instant;
fn busy() -> u64 {
(0..std::hint::black_box(1_000_000_000u64)).fold(0, |a, x| a ^ x.wrapping_mul(31))
}
async fn cpu_job() -> u64 {
busy() // .await を挟まないCPU処理
}
#[tokio::main(flavor = "multi_thread", worker_threads = 4)]
async fn main() {
let t = Instant::now();
let (a, b) = tokio::join!(cpu_job(), cpu_job());
println!("join! : {:.2}秒 ({})", t.elapsed().as_secs_f64(), a == b);
let t = Instant::now();
let h1 = tokio::spawn(cpu_job());
let h2 = tokio::spawn(cpu_job());
let (a, b) = (h1.await.unwrap(), h2.await.unwrap());
println!("spawn : {:.2}秒 ({})", t.elapsed().as_secs_f64(), a == b);
}
join! : 0.57秒 (true)
spawn : 0.28秒 (true)
Intel Core i9-9880H(16論理コア)でreleaseビルドを3回実行すると、join!は0.55〜0.86秒、spawnは0.28〜0.42秒で、spawnが毎回ほぼ半分でした。join!の2本は同じスレッドで順番に計算され、この実測ではspawnの2本が2つのワーカーで同時に計算されたためです。I/O待ちを重ねるだけならjoin!で足ります。数秒かかる計算はspawnでもワーカーを占有して他のタスクを止めるので、spawn_blockingかrayonに渡します。
tokio::spawnのJoinHandleとJoinSetによる回収
tokio::spawnはFutureを独立したタスクとしてランタイムに登録し、JoinHandleを返します。.awaitするとResult<T, JoinError>が返り、タスク内のpanicはErrとして受け取れます。複数のタスクを投げて終わった順に回収するならJoinSetが便利です。
use std::time::Duration;
use tokio::task::JoinSet;
#[tokio::main]
async fn main() {
let mut set = JoinSet::new();
for id in [3u64, 1, 2] {
set.spawn(async move {
tokio::time::sleep(Duration::from_millis(id * 100)).await;
id
});
}
while let Some(res) = set.join_next().await {
println!("完了: {}", res.unwrap());
}
}
完了: 1
完了: 2
完了: 3
登録順は3→1→2ですが、回収は待ち時間の短い順になりました。JoinSetをdropすると、残っているタスクはすべて中断されます。
timeoutとselect!によるキャンセル
前述のとおり、Futureはdropすると以後進みません(spawn済みのタスクを止めるにはJoinHandle::abortを呼びます)。tokio::time::timeoutは、期限までに終わらなかったFutureをdropしてErr(Elapsed)を返します。期限の判定は内側のFutureをpollしたあとに行うため、.awaitを挟まず実行権を返さない処理は、期限を過ぎてもOkで完了することがあります(tokio::time::timeoutのドキュメント)。
use std::time::Duration;
use tokio::time::{sleep, timeout};
async fn slow() -> &'static str {
sleep(Duration::from_secs(3)).await;
"slow の結果"
}
#[tokio::main]
async fn main() {
match timeout(Duration::from_secs(1), slow()).await {
Ok(v) => println!("{v}"),
Err(e) => println!("タイムアウト: {e}"),
}
}
タイムアウト: deadline has elapsed
tokio::select!は、完了した結果がパターンに一致した枝のハンドラを実行します。所有している他のFutureは破棄しますが、借用して渡したFuture本体は残ります。
use std::time::Duration;
use tokio::sync::mpsc;
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel::<&str>(8);
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(300)).await;
tx.send("メッセージ").await.unwrap();
});
tokio::select! {
Some(msg) = rx.recv() => println!("受信: {msg}"),
_ = tokio::time::sleep(Duration::from_millis(100)) => println!("100msで打ち切り"),
}
}
100msで打ち切り
select!で注意するのは、途中まで進んだ処理が捨てられることです。Tokioは各メソッドのドキュメントに「Cancel safety」の節を設けており、mpsc::Receiver::recvは「cancel safe(負けた枝では1件も受信していない)」、AsyncReadExt::read_exactは「not cancel safe(読みかけのデータが失われうる)」と明記しています。select!の枝に置く前に、その節を確認してください。
async-stdの開発終了とランタイムの選び方
Tokioとasync-stdを並べて比較する解説は、もう前提が成り立ちません。async-stdのGitHubリポジトリのREADME冒頭は「async-std has been discontinued; use smol instead」となっており、2025年3月15日のv1.13.1で開発終了を告知しました。crates.ioの説明文もDeprecated in favor of `smol`に変わっています。
| ランタイム | 最新版(2026年10月25日) | 位置づけ |
|---|---|---|
| Tokio | 1.54.1 | 事実上の標準。Axum・tonic・reqwest等が前提 |
| smol | 2.0.2 | 小型のランタイム。async-stdの移行先 |
| futures::executor | futures 0.3.34 | block_on・LocalPool等。I/Oドライバなし |
| async-std | 1.13.2 | 開発終了。新規採用しない |
Axum・tonic・reqwestを中心に構成するなら、依存関係を揃えやすいTokioを推奨します。Axumもtonicも内部でTokioに依存しており、別ランタイムを選ぶと互換レイヤーが必要になります(Axumとは?Rust製Webフレームワークの特徴・使い方・Actix Web/Rocket比較を解説、gRPC Rustの実装|tonic 0.14のクレート分割とTLS・相互接続を解説)。Tokioは固定の公開日程を設けず、通常は月1回程度マイナー版を公開しています。2026年10月19日の1.54.0でMSRV(対応する最低のRust)がRust 1.85に上がりました(Tokio 1.54.0のリリースノート)。版を固定したい場合は、LTSの1.51.x(2027年3月まで)か1.53.x(2027年9月まで)をtokio = "~1.53"のようにチルダ指定します。2系統ともMSRVはRust 1.71のままで、2026年10月13日に1.51.6と1.53.3が出ています。LTSの一覧はTokioのGitHubリポジトリのREADMEで確認してください。
async fn in traitとasync-traitクレートの使い分け
Rust 1.75で標準化されたasync fn in traitとdyn非対応
Rust 1.75.0(2023年12月28日)から、トレイトのメソッドに直接async fnを書けます。以前は必須だったasync-traitクレートがなくても、ジェネリクス経由の静的ディスパッチなら動きます。一方でBox<dyn Trait>にしようとすると、次のエラーになります。
trait Fetcher {
async fn fetch(&self) -> String;
}
struct Api;
impl Fetcher for Api {
async fn fetch(&self) -> String { "ok".into() }
}
fn main() {
let _list: Vec<Box<dyn Fetcher>> = vec![Box::new(Api)];
}
error[E0038]: the trait `Fetcher` is not dyn compatible
--> src/bin/dyntrait.rs:11:28
|
11 | let _list: Vec<Box<dyn Fetcher>> = vec![Box::new(Api)];
| ^^^^^^^ `Fetcher` is not dyn compatible
|
note: for a trait to be dyn compatible it needs to allow building a vtable
for more information, visit <https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility>
--> src/bin/dyntrait.rs:2:14
|
1 | trait Fetcher {
| ------- this trait is not dyn compatible...
2 | async fn fetch(&self) -> String;
| ^^^^^ ...because method `fetch` is `async`
= help: consider moving `fetch` to another trait
= help: only type `Api` implements `Fetcher`; consider using it directly instead.
async fnが返すFutureの型は実装ごとに違い、vtableに1つの型として載せられないためです。
Sendを要求する場面の書き換えとtrait-variant
もう1つの制約はSendです。トレイト定義のasync fnは、返すFutureがSendかどうかを約束しません。そのためジェネリクスで受け取った実装をtokio::spawnに渡すと失敗します。
trait Fetcher { async fn fetch(&self) -> String; }
async fn run<F: Fetcher + Send + Sync + 'static>(f: &'static F) {
tokio::spawn(async move { f.fetch().await });
}
fn main() {}
error: future cannot be sent between threads safely
--> src/bin/nosend.rs:4:5
|
4 | tokio::spawn(async move { f.fetch().await });
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ future created by async block is not `Send`
|
= help: within `{async block@src/bin/nosend.rs:4:18: 4:28}`, the trait `Send` is not implemented for `impl Future<Output = String>`
note: future is not `Send` as it awaits another future which is not `Send`
--> src/bin/nosend.rs:4:31
|
4 | tokio::spawn(async move { f.fetch().await });
| ^^^^^^^^^ await occurs here on type `impl Future<Output = String>`, which is not `Send`
help: `Send` can be made part of the associated future's guarantees for all implementations of `Fetcher::fetch`
|
1 - trait Fetcher { async fn fetch(&self) -> String; }
1 + trait Fetcher { fn fetch(&self) -> impl std::future::Future<Output = String> + Send; }
|
rustcのhelpが示すとおり、トレイト側の宣言をfn fetch(&self) -> impl Future<Output = String> + Send;に書き換えれば、実装側はasync fnのままでtokio::spawnに渡せます。
use std::future::Future;
trait Fetcher {
fn fetch(&self) -> impl Future<Output = String> + Send;
}
struct Api;
impl Fetcher for Api {
async fn fetch(&self) -> String { "api".into() }
}
async fn run<F: Fetcher + Sync + 'static>(f: &'static F) {
tokio::spawn(async move { println!("{}", f.fetch().await) }).await.unwrap();
}
#[tokio::main]
async fn main() {
static API: Api = Api;
run(&API).await;
}
api
この宣言にすると、Rcを握ったまま.awaitする実装のように、Sendでない実装はコンパイルできなくなります。呼び出し側でF::fetch(..): Sendと境界を書くreturn type notation(tracking issue #109417)は、2026年10月時点でnightly限定です。
Send版と非Send版の両方を公開したいときの手段として、Rustの公式ブログ(2023年12月21日)はrust-lang製のtrait-variantクレートを案内しています。#[trait_variant::make]を付けると、戻り値にSendを付けた版のトレイトが自動生成されます。
#[trait_variant::make(Fetcher: Send)]
trait LocalFetcher {
async fn fetch(&self) -> String;
}
struct Api;
impl Fetcher for Api {
async fn fetch(&self) -> String { "api".into() }
}
async fn run<F: Fetcher + Sync + 'static>(f: &'static F) {
tokio::spawn(async move { println!("{}", f.fetch().await) }).await.unwrap();
}
#[tokio::main]
async fn main() {
static API: Api = Api;
run(&API).await;
}
dyn Traitが必要な場合のasync-trait
プラグインのように実装を実行時に切り替えたい場合は、今もasync-traitクレート(0.1.92)を使います。トレイトと実装の両方に属性を付けるだけで、先ほどエラーになったVec<Box<dyn Fetcher>>が通ります。
#[async_trait::async_trait]
trait Fetcher {
async fn fetch(&self) -> String;
}
struct Api;
#[async_trait::async_trait]
impl Fetcher for Api {
async fn fetch(&self) -> String { "api".into() }
}
struct Cache;
#[async_trait::async_trait]
impl Fetcher for Cache {
async fn fetch(&self) -> String { "cache".into() }
}
#[tokio::main]
async fn main() {
let list: Vec<Box<dyn Fetcher>> = vec![Box::new(Api), Box::new(Cache)];
for f in &list {
println!("{}", f.fetch().await);
}
}
api
cache
代わりに、async-traitはメソッドの戻り値をPin<Box<dyn Future<Output = String> + Send + 'async_trait>>に書き換えるため、呼び出しのたびにヒープ確保が1回発生します。静的ディスパッチで足りる箇所まで付けて回る必要はありません。
asyncクロージャとAsyncFn(Rust 1.85)
Rust 1.85.0(2025年2月20日)でasync |x| ...形式のクロージャと、それを受け取るためのAsyncFn・AsyncFnMut・AsyncFnOnceトレイトが安定化しました。以前の|x| async move { ... }と違い、クロージャが捕捉した変数を返すFutureの中から借用できます。
async fn call_twice<F: AsyncFn(u32) -> u32>(f: F) -> u32 {
f(1).await + f(2).await
}
#[tokio::main]
async fn main() {
let base = 10;
let add = async |x: u32| x + base; // Rust 1.85で安定化した async クロージャ
println!("{}", call_twice(add).await);
}
23
nightlyで動かすコルーチン・async gen・AsyncDrop
2026年10月25日時点の安定版Rust 1.99.1には、yieldを書けるコルーチン、非同期版のイテレータ、非同期のデストラクタのどれもありません。いずれもnightly限定の機能で、安定版で#![feature(...)]を書くとerror[E0554]: `#![feature]` may not be used on the stable release channelで止まります。ここではnightly(rustc 1.101.0-nightly、2026年10月23日ビルド)で実際に動かした結果を載せます。
| 機能 | feature名 | tracking issue |
|---|---|---|
| yieldを書けるコルーチン | coroutines | #43122 |
| genブロック | gen_blocks | #117078 |
| AsyncIterator | async_iterator | #79024 |
| AsyncDrop | async_drop | #126482 |
| return type notation | return_type_notation | #109417 |
yieldを書けるコルーチンとgenブロック
Rustのコルーチンは、呼び出し側がresumeを呼ぶたびに、前回のyieldの続きから実行を進める構文です。#[coroutine]を付けたクロージャの中でyieldした値はCoroutineState::Yieldedとして返り、本体の最後まで進むとCompleteになります。
#![feature(coroutines, coroutine_trait, stmt_expr_attributes)]
use std::ops::{Coroutine, CoroutineState};
use std::pin::pin;
fn main() {
let mut co = pin!(#[coroutine] || {
yield 1;
yield 2;
"終わり"
});
loop {
match co.as_mut().resume(()) {
CoroutineState::Yielded(v) => println!("Yielded({v})"),
CoroutineState::Complete(r) => { println!("Complete({r})"); break; }
}
}
}
Yielded(1)
Yielded(2)
Complete(終わり)
イテレータを作るだけなら、genブロックのほうが短く書けます。genはRust 2024 editionで予約語になっており、edition 2024のコードではgenという名前の変数や関数にr#genと書く必要があります。
#![feature(gen_blocks)]
fn evens(max: u32) -> impl Iterator<Item = u32> {
gen move {
for n in 0..=max {
if n % 2 == 0 { yield n; }
}
}
}
fn main() {
let v: Vec<u32> = evens(8).collect();
println!("{v:?}");
}
[0, 2, 4, 6, 8]
async genブロックとAsyncIterator
async genブロックは、中で.awaitとyieldの両方を書けるブロックで、std::async_iter::AsyncIteratorを返します。for awaitの構文は使わず、poll_nextをpoll_fnで包んで1件ずつ取り出しました。
#![feature(async_iterator, gen_blocks)]
use std::async_iter::AsyncIterator;
use std::future::poll_fn;
use std::pin::pin;
use std::time::Duration;
#[tokio::main]
async fn main() {
let mut s = pin!(async gen {
for i in 1..=3 {
tokio::time::sleep(Duration::from_millis(100)).await;
yield i * 10;
}
});
while let Some(v) = poll_fn(|cx| s.as_mut().poll_next(cx)).await {
println!("{v}");
}
}
10
20
30
安定版で同じことをするなら、futuresクレートのStreamトレイトとStreamExtを使います。前述のpin!の例がその形です。tokio-streamやtonicのストリーム型も、このStreamトレイトを実装しています。
AsyncDropの挙動と安定版での後片付け
AsyncDropは、値が破棄されるときに.awaitを含む後片付けを走らせるトレイトです。接続のクローズを模した型で試しました。
#![feature(async_drop)]
use std::future::AsyncDrop;
use std::pin::Pin;
struct Conn(&'static str);
impl Drop for Conn {
fn drop(&mut self) { println!("{}: 同期のdrop", self.0); }
}
impl AsyncDrop for Conn {
async fn drop(self: Pin<&mut Self>) {
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
println!("{}: 非同期のdropが完了", self.0);
}
}
async fn work() {
let _c = Conn("接続A");
println!("work の本体");
}
#[tokio::main]
async fn main() {
work().await;
println!("main の終わり");
}
warning: the feature `async_drop` is incomplete and may not be safe to use and/or cause compiler crashes
--> src/bin/asyncdrop.rs:1:12
|
1 | #![feature(async_drop)]
| ^^^^^^^^^^
|
= note: see issue #126482 <https://github.com/rust-lang/rust/issues/126482> for more information
= note: `#[warn(incomplete_features)]` on by default
work の本体
接続A: 非同期のdropが完了
main の終わり
async関数の中で落ちた「接続A」では、同期のDropは呼ばれず非同期のdropだけが走りました。同じ型を同期のfn mainの中で落とすと、同期のDropが呼ばれて「接続B: 同期のdrop」が出力されます。コンパイラはasync_dropを「incomplete(未完成でコンパイラがクラッシュする可能性がある)」と警告しており、本番のコードで使う段階ではありません。
安定版のDrop::dropの中では.awaitできません。非同期の後片付けが要る型は、async fn close(self)のような明示的なメソッドを用意し、呼び出し側で.awaitする設計にします。
async Rustでつまずくコンパイルエラーと直し方
ここからは、実際に遭遇しやすいエラーを再現し、メッセージと直し方を対にしてまとめます。エラー文は英語のまま長く出ますが、見るべき行は決まっています。掲載したエラーは、ローカルのパスを含むnote: required by a bound in `tokio::spawn`以降の行と末尾の要約行を省いています。
MutexGuardを.awaitの先まで持つとSendエラー
use std::sync::{Arc, Mutex};
async fn save(_v: u32) {}
#[tokio::main]
async fn main() {
let counter = Arc::new(Mutex::new(0u32));
let c = counter.clone();
tokio::spawn(async move {
let mut guard = c.lock().unwrap();
*guard += 1;
save(*guard).await; // ガードを保持したまま .await
});
}
error: future cannot be sent between threads safely
--> src/bin/mutex.rs:9:5
|
9 | / tokio::spawn(async move {
10 | | let mut guard = c.lock().unwrap();
11 | | *guard += 1;
12 | | save(*guard).await; // ガードを保持したまま .await
13 | | });
| |______^ future created by async block is not `Send`
|
= help: within `{async block@src/bin/mutex.rs:9:18: 9:28}`, the trait `Send` is not implemented for `std::sync::MutexGuard<'_, u32>`
note: future is not `Send` as this value is used across an await
--> src/bin/mutex.rs:12:22
|
10 | let mut guard = c.lock().unwrap();
| --------- has type `std::sync::MutexGuard<'_, u32>` which is not `Send`
11 | *guard += 1;
12 | save(*guard).await; // ガードを保持したまま .await
| ^^^^^ await occurs here, with `mut guard` maybe used later
読むべきはnote: future is not `Send` as this value is used across an awaitの行です。std::sync::MutexGuardはSendではなく、それを保持したまま.awaitするとFuture全体がSendでなくなります。値を取り出したらブロックを閉じてガードを先に落とします。
tokio::spawn(async move {
let v = {
let mut guard = c.lock().unwrap();
*guard += 1;
*guard
}; // ここでガードを落とす
save(v).await;
}).await.unwrap();
ロックを握ったまま.awaitする設計がどうしても必要なら、tokio::sync::Mutexを使います。ただし標準のMutexより遅いため、Tokioのドキュメントも「ロックを.awaitの先まで持つ必要が無ければ標準のMutexでよい」という立場です。
spawnに借用を渡すとE0373
#[tokio::main]
async fn main() {
let name = String::from("issoh");
let handle = tokio::spawn(async {
println!("{name}");
});
handle.await.unwrap();
}
error[E0373]: async block may outlive the current function, but it borrows `name`, which is owned by the current function
--> src/bin/borrow.rs:4:31
|
4 | let handle = tokio::spawn(async {
| ^^^^^ may outlive borrowed value `name`
5 | println!("{name}");
| ---- `name` is borrowed here
|
= note: async blocks are not executed immediately and must either take a reference or ownership of outside variables they use
help: to force the async block to take ownership of `name` (and any other referenced variables), use the `move` keyword
|
4 | let handle = tokio::spawn(async move {
| ++++
tokio::spawnのタスクは呼び出し元より長生きする可能性があるので、ローカル変数を借用できません('static要件)。async moveで所有権ごと渡すか、複数のタスクで共有するならArcに包んでcloneしてから渡します。
async fnの再帰でE0733
error[E0733]: recursion in an async fn requires boxing
--> src/bin/recursion.rs:1:1
|
1 | async fn fib(n: u32) -> u64 {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
2 | if n < 2 { return n as u64; }
3 | fib(n - 1).await + fib(n - 2).await
| ---------------- recursive call here
|
= note: a recursive `async fn` call must introduce indirection such as `Box::pin` to avoid an infinitely sized future
Futureは状態をすべて自分の中に持つため、自分自身を含むと大きさが無限になります。Rust 1.77.0以降は、再帰呼び出しをBox::pinで包んでヒープに逃がせばasync fnのまま書けます(値がヒープへ移る条件はRustのスタックとヒープ|値がヒープへ脱出する条件とメモリ領域の使い分けを参照)。
async fn fib2(n: u32) -> u64 {
if n < 2 { return n as u64; }
Box::pin(fib2(n - 1)).await + Box::pin(fib2(n - 2)).await
}
非同期の中でblock_onするとpanic
同期関数から非同期関数を呼びたくなり、#[tokio::main]の内側で別途作成したTokioランタイムのblock_onを直接呼ぶと、コンパイルは通っても実行時にpanicします。
Cannot start a runtime from within a runtime. This happens because a function (like `block_on`) attempted to block the current thread while the thread is being used to drive asynchronous tasks.
非同期の文脈にいるなら.awaitで呼び、同期処理を挟みたいならspawn_blockingを使います。Tokioのblock_onは非同期処理を実行中のスレッドから直接呼べませんが、spawn_blocking内などからHandle::block_onで非同期処理へ戻ることはできます。
async/awaitを使わないほうがよい場面
asyncが効くのは、待ち時間の大半がネットワークやディスクのI/Oで、同時に多数の接続をさばく処理です。逆の条件では導入しないほうが保守しやすくなります。
- 画像変換や圧縮などCPUを使い切る処理:Futureを分割しても計算量は減らず、ワーカースレッドを占有して他のタスクを止めます。rayonなどのスレッドプールか
spawn_blockingに任せます。 - 数本のファイルを順に読むだけのCLI:待ち時間を重ねる処理がなければ、ランタイムを追加しても高速化は期待しにくく、構成が複雑になります。
- 処理の大半が同期APIへの逐次呼び出しで、他の非同期処理と並行させる必要がない場合:
spawn_blockingとの橋渡しが増えるため、同期構成を優先します。
同期と非同期の選び方を言語横断で整理したい場合は非同期処理とは?同期処理との違いから実装方式まで実装者目線で解説が参考になります。Rustのasyncはスレッドを作らずに状態マシンで待ちを重ねる設計で、その恩恵はゼロコスト抽象化とは?Rustで抽象度と実行速度を両立する仕組みで説明している考え方の延長にあります。同時接続の多いAPIサーバーやバッチの並行化をRustで組む案件は、フルスクラッチ開発として要件の整理から対応しています。
よくある質問
Rustのawaitはどう書きますか?
future.awaitのように、式の後ろに.awaitを付けます。JavaScriptやPythonの前置awaitと違い後置構文なので、client.get(url).send().await?.json().await?のようにメソッドチェーンの途中に挟めます。書けるのはasync fn、asyncブロック、非同期クロージャの中です。
Rustの標準ライブラリだけでasyncは動きますか?
標準ライブラリにあるのはFutureトレイトやWakerなどの部品だけで、完成品のexecutorも非同期I/Oも含まれていません。部品を組み合わせて自作のexecutorを書けば動かせますが、通常はTokioやsmolのようなランタイムか、futuresクレートのfutures::executor::block_onを使います。
async-stdはまだ使えますか?
既存コードはビルドできますが、2025年3月のv1.13.1で開発終了が告知され、公式にsmolへの移行が推奨されています。新規のプロジェクトではTokioかsmolを選んでください。
Rustのasync traitは標準で書けますか?
Rust 1.75.0以降は、トレイトのメソッドにasync fnを直接書けます。ただしdyn Traitとして使えない点と、トレイトの宣言だけでは戻り値のFutureがSendであることを保証できない点は残っています。前者はasync-traitクレートで補い、後者はトレイト側をfn fetch(&self) -> impl Future<Output = String> + Sendの形で宣言するか、trait-variantクレートで補います。
Rustのasyncとコルーチンはどう関係していますか?
async fnはコンパイラ内部でコルーチンに変換され、.awaitの位置で区切られた状態マシンになります。一方、yieldで値を返す汎用コルーチン構文とCoroutineトレイトは、2026年10月時点(Rust 1.99.1)でもnightly限定(#![feature(coroutines)]、tracking issue #43122)で、安定版で書くとerror[E0554]になります。