tokio(非同期ランタイム)
tokioとは
tokio は、[[Rust]] の非同期処理を実際に走らせる土台(ランタイム)です。Rust の async / await は言語機能として構文だけを提供し、「いつどのタスクを進めるか」を決める仕組みは標準ライブラリに含まれていません。その役目を担うのが tokio です。
#[tokio::main]
async fn main() {
let body = reqwest::get("https://example.com").await.unwrap().text().await.unwrap();
println!("{} bytes", body.len());
}
#[tokio::main] は、通常の main 関数の中にランタイムを起動して非同期関数を実行するコードを展開するマクロです。これを付け忘れると async fn を書いても何も走りません。
なぜランタイムが要るのか
async fn は呼んだ時点では何も実行しません。呼び出しの結果は Future という「まだ完了していない計算」を表す値で、誰かが繰り返し進行を促さないと前に進みません。この促す役目がランタイムです。
| 担当 | 誰がやるか |
|---|---|
async / await の構文と Future 型 | Rust 本体(言語機能) |
| タスクの実行順の決定・OSスレッドへの割り当て | ランタイム(tokio など) |
| ネットワークやタイマーの完了通知の受け取り | ランタイム(epoll・kqueue などを利用) |
[[Node.js]] のようにランタイムが言語処理系へ最初から組み込まれている環境と違い、Rust は用途に応じてランタイムを選べる設計になっています。汎用サーバー用途では tokio が事実上の標準です。
スレッドとの使い分け
tokio が解くのは「待ち時間が長い処理を大量に同時に抱える」問題です。[[プロセスとスレッド]] の考え方でいえば、OSスレッドは数千個も作ると切り替えコストとメモリで破綻しますが、非同期タスクは待ちの間スレッドを占有しないため、少数のスレッド上で数万件を扱えます。
- I/O待ちが主(ネットワーク・ファイル・DB) — tokio のタスクが向く。待っている間、そのスレッドは別のタスクを進められる
- CPUを使い続ける計算が主 — 非同期にしても速くならない。
spawn_blockingで専用スレッドへ逃がす
CPUを使う処理を非同期タスクの中に直接書くのは事故のもとです。 そのタスクが await に到達するまでスレッドを手放さないため、同じスレッドに載っている他のタスクがまとめて止まります。重い計算やブロッキングAPIの呼び出しは spawn_blocking で分離します。
実務での使いどころ
- [[Webサーバーとアプリケーションサーバー]] — axum・tonic など主要なサーバーフレームワークが tokio 上で動く
- デスクトップアプリのバックグラウンド処理 — Tauri は tokio を内蔵し、UIを止めずにファイル走査やHTTP通信を行う
- タイマー・タイムアウト・並行待ち —
tokio::time::timeoutやtokio::join!で、待ち時間の上限や複数処理の同時待ちを短く書ける
use std::time::Duration;
use tokio::time::timeout;
// 3秒で打ち切る。超えると Err が返る
let result = timeout(Duration::from_secs(3), fetch_user(id)).await;
タイムアウトは [[エラーハンドリングとリトライ設計]] の入口です。上限を設けずに待つと、相手側の不調がそのまま自分のタスク滞留になります。
初学者向けポイント
- 依存に書く機能(feature)を絞る —
fullは全部入りで便利ですが、必要なもの(rt-multi-thread・net・timeなど)だけを有効にするとビルド時間とバイナリサイズが減ります asyncは伝染する —awaitを使うには呼び出し元もasyncである必要があります。どこを非同期にするかは後から広げにくいので、境界を先に決めます- ライブラリのランタイムを混ぜない — 別のランタイム前提のライブラリと組み合わせると、実行時に「ランタイムが見つからない」というエラーになります
std::thread::sleepを使わない — 非同期タスクの中で呼ぶとスレッドごと止まります。tokio::time::sleepを使います
関連技術とのつながり
- [[Rust]] —
async/awaitの構文は言語側、実行はランタイム側という分担になっている - [[並行処理と非同期]] — 「同時に進める」ための方式のうち、非同期I/Oを担う実装
- [[プロセスとスレッド]] — OSスレッドの限界を、待ち時間を手放すタスクで置き換える
- [[Webサーバーとアプリケーションサーバー]] — Rust製サーバーフレームワークの実行基盤
- [[エラーハンドリングとリトライ設計]] — タイムアウトとリトライを組み合わせて滞留を防ぐ
- [[Cargoワークスペース構成]] — 非同期を使うクレートと使わないクレートを分けるとビルドが軽くなる
Q: Rustで `async fn` を書いてもランタイムが必要な理由はどれ?
- [ ] `async` 構文がコンパイルできないから
- [x] `async fn` の呼び出しはFutureを返すだけで、進行を促す仕組みが標準ライブラリに無いから
- [ ] 非同期処理にはOSの管理者権限が要るから
解説: Rustは構文とFuture型だけを提供し、実行順の決定やI/O完了通知の受け取りはランタイムに任せる分担になっています。
Q: tokioのタスクが向いている処理はどれ?
- [ ] 画像のエンコードのようにCPUを使い続ける処理
- [x] ネットワークやDBの応答待ちが長い処理
- [ ] 起動時に一度だけ走る設定読み込み
解説: 待っている間スレッドを手放せるため、待ち時間の長い処理を大量に同時に抱えられます。CPUを使い続ける処理は `spawn_blocking` で分離します。
Q: 非同期タスクの中で `std::thread::sleep` を呼ぶと何が起きる?
- [ ] そのタスクだけが待機し、他のタスクは進む
- [x] スレッドごと止まり、同じスレッド上の他のタスクも進まなくなる
- [ ] コンパイルエラーになる
解説: `std::thread::sleep` はスレッドを占有します。非同期タスク内では `tokio::time::sleep` を使い、待っている間スレッドを手放します。