はじめに
Rustの async/await、最初に見たとき「なにこれ...」ってなりませんでした?
私はなりました。
async fn fetch_data() -> String { ... }
#[tokio::main]
async fn main() {
let data = fetch_data().await;
}
「asyncってなに?」「awaitってなに?」「tokio::main ってなに?」
この記事では、私が async/await を理解するまでの7ステップを紹介します。
目次
- Step 1: なぜ非同期処理が必要か
- Step 2: Future とは
- Step 3: async/await の基本
- Step 4: ランタイムが必要な理由
- Step 5: 並行処理のパターン
- Step 6: よくあるハマりポイント
- Step 7: 実践的な使い方
Step 1: なぜ非同期処理が必要か
同期処理の問題
fn fetch_user() -> User {
// 1秒かかるとする
std::thread::sleep(std::time::Duration::from_secs(1));
User { name: "Alice".to_string() }
}
fn fetch_posts() -> Vec<Post> {
// 1秒かかるとする
std::thread::sleep(std::time::Duration::from_secs(1));
vec![Post { title: "Hello".to_string() }]
}
fn main() {
let user = fetch_user(); // 1秒待つ
let posts = fetch_posts(); // さらに1秒待つ
// 合計2秒
}
問題: I/O待ちの間、CPUが遊んでいる。もったいない。
非同期なら並行実行できる
async fn fetch_user() -> User { ... }
async fn fetch_posts() -> Vec<Post> { ... }
#[tokio::main]
async fn main() {
// 同時に開始
let (user, posts) = tokio::join!(fetch_user(), fetch_posts());
// 合計1秒(並行実行)
}
Step 2: Future とは
Future トレイト
async fn が返すのは 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, // まだ
}
重要な性質
Future は遅延評価。呼んだだけでは実行されない。
async fn hello() {
println!("Hello!");
}
fn main() {
let future = hello(); // ← 何も表示されない!
// futureを.awaitするか、ランタイムに渡さないと実行されない
}
hello() を呼んでも Future が返るだけ。実行するには .await が必要。
Step 3: async/await の基本
async fn
async fn は Future を返す関数を定義する糖衣構文:
// これと
async fn fetch() -> String {
"data".to_string()
}
// これは大体同じ
fn fetch() -> impl Future<Output = String> {
async {
"data".to_string()
}
}
.await
Future を実行して結果を待つ:
async fn main() {
let data = fetch().await; // fetchが完了するまで待つ
println!("{}", data);
}
async ブロック
関数じゃなくてもasyncは書ける:
let future = async {
let a = fetch_a().await;
let b = fetch_b().await;
a + b
};
Step 4: ランタイムが必要な理由
Rustの非同期はゼロコスト...だが
Rust自体には非同期ランタイムが含まれていない。
// これだけではコンパイルエラー
fn main() {
let data = fetch().await; // main()はasyncじゃない
}
ランタイムの役割
- Futureのスケジューリング: どのFutureを実行するか決める
- I/O多重化: epoll/kqueue などで効率的にI/O待ち
-
タイマー管理:
sleepとかtimeoutとか
代表的なランタイム
| ランタイム | 特徴 |
|---|---|
| tokio | 最も人気。フル機能。 |
| async-std | stdに近いAPI。 |
| smol | 軽量。シンプル。 |
tokio の使い方
[dependencies]
tokio = { version = "1", features = ["full"] }
#[tokio::main]
async fn main() {
let data = fetch().await;
}
// ↑ は以下と同じ
fn main() {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let data = fetch().await;
});
}
Step 5: 並行処理のパターン
パターン1: join!(全部待つ)
use tokio::join;
async fn main() {
let (a, b, c) = join!(
fetch_a(),
fetch_b(),
fetch_c(),
);
// 全部完了するまで待つ
}
パターン2: select!(最初の1つ)
use tokio::select;
async fn main() {
select! {
result = fetch_a() => println!("A finished: {:?}", result),
result = fetch_b() => println!("B finished: {:?}", result),
}
// 最初に完了したものだけ処理
}
パターン3: spawn(バックグラウンド実行)
#[tokio::main]
async fn main() {
let handle = tokio::spawn(async {
// バックグラウンドで実行
heavy_computation().await
});
// 他の処理
do_something().await;
// 結果を待つ
let result = handle.await.unwrap();
}
パターン4: 複数タスクの並行処理
use futures::future::join_all;
async fn fetch_all(urls: Vec<String>) -> Vec<Response> {
let futures: Vec<_> = urls.iter()
.map(|url| fetch(url))
.collect();
join_all(futures).await
}
Step 6: よくあるハマりポイント
ハマり1: async fn は Send でないといけない(場合がある)
async fn bad_example() {
let rc = std::rc::Rc::new(42); // RcはSendじゃない
tokio::spawn(async move {
println!("{}", rc); // コンパイルエラー!
}).await;
}
tokio::spawn に渡す Future は Send が必要。Rc は Send じゃないのでエラー。
解決策: Arc を使う。
async fn good_example() {
let arc = std::sync::Arc::new(42);
tokio::spawn(async move {
println!("{}", arc);
}).await;
}
ハマり2: .await の位置
// ❌ 間違い: ロックを持ったまま.await
async fn bad() {
let mutex = tokio::sync::Mutex::new(0);
let mut guard = mutex.lock().await;
*guard += 1;
some_async_fn().await; // ロックを持ったまま待機
} // ここでロック解放
// ⭕ 正しい: .await前にロック解放
async fn good() {
let mutex = tokio::sync::Mutex::new(0);
{
let mut guard = mutex.lock().await;
*guard += 1;
} // ここでロック解放
some_async_fn().await;
}
ハマり3: blocking 処理
// ❌ 間違い: asyncの中でブロッキング処理
async fn bad() {
std::thread::sleep(Duration::from_secs(1)); // ランタイムをブロック!
}
// ⭕ 正しい: tokio::time::sleepを使う
async fn good() {
tokio::time::sleep(Duration::from_secs(1)).await;
}
// ⭕ またはspawn_blockingで別スレッドに逃がす
async fn also_good() {
tokio::task::spawn_blocking(|| {
heavy_cpu_work() // CPUバウンドな処理
}).await.unwrap();
}
ハマり4: async トレイトメソッド
// 現在のRustでは直接書けない
trait MyTrait {
async fn do_something(&self); // エラー!
}
解決策: async-trait クレートを使う
use async_trait::async_trait;
#[async_trait]
trait MyTrait {
async fn do_something(&self);
}
#[async_trait]
impl MyTrait for MyStruct {
async fn do_something(&self) {
// ...
}
}
Step 7: 実践的な使い方
HTTPクライアント(reqwest)
[dependencies]
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }
use reqwest;
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let body = reqwest::get("https://httpbin.org/get")
.await?
.text()
.await?;
println!("{}", body);
Ok(())
}
タイムアウト
use tokio::time::{timeout, Duration};
async fn with_timeout() -> Result<String, &'static str> {
match timeout(Duration::from_secs(5), fetch_data()).await {
Ok(result) => Ok(result),
Err(_) => Err("タイムアウト"),
}
}
リトライ
async fn fetch_with_retry(url: &str, max_retries: u32) -> Result<String, Error> {
let mut attempts = 0;
loop {
match fetch(url).await {
Ok(data) => return Ok(data),
Err(e) if attempts < max_retries => {
attempts += 1;
tokio::time::sleep(Duration::from_secs(1 << attempts)).await;
}
Err(e) => return Err(e),
}
}
}
チャネル
use tokio::sync::mpsc;
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel(32);
tokio::spawn(async move {
for i in 0..10 {
tx.send(i).await.unwrap();
}
});
while let Some(value) = rx.recv().await {
println!("received: {}", value);
}
}
まとめ
7ステップのおさらい
- なぜ非同期か: I/O待ちを効率化
- Future: 遅延評価される計算
- async/await: Future を楽に書く構文
- ランタイム: Future を実行する仕組み
- 並行処理: join!, select!, spawn
- ハマりポイント: Send, ロック, blocking
- 実践: reqwest, timeout, channel
チェックリスト
-
async fnは Future を返すだけで、呼んでも実行されない -
.awaitで実際に実行される - ランタイム(tokio等)がないと動かない
-
blocking 処理は
spawn_blockingで逃がす -
std::sync::Mutexじゃなくてtokio::sync::Mutexを使う
今すぐできるアクション
- tokio を入れて簡単な async/await を書いてみる
-
join!で並行処理を体験 - reqwest で HTTP リクエストを投げてみる
async/await、最初は意味不明だったけど、「Future = 遅延評価」「ランタイムが実行する」がわかったら急に理解が進みました。
みなさんも頑張ってね!
この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!