Tokio on Ubuntu — Rust非同期ランタイムの仕組みと実践パターン

開発環境

「Rustで非同期処理を書きたいけど、async/awaittokio が何をしているのかよくわからない」——Ubuntu上でRustを触り始めた方が最初に詰まるのが、まさにここです。本記事では、Ubuntu 24.04 LTSに実際にTokioをインストールし、動かしながら非同期ランタイムの仕組みを理解することを目指します。

結論からいうと、Cargo.tomltokio = { version = "1", features = ["full"] } を足して #[tokio::main] を付けるだけで、非同期エントリーポイントが完成します。本記事の数値・ログはすべて Docker公式イメージ ubuntu:24.04(arm64)で実際にビルド・実行して取得した実測値です。想像で書いた数字は一つもありません。

この記事のポイント

  • Ubuntu 24.04 LTS では apt install cargo build-essential で Rust 1.75.0 が入る
  • Tokioは非同期タスクをOSスレッドより軽量に多重化する「ランタイム」。標準ライブラリには含まれない
  • tokio::spawn でタスクを生成し、.await で結果を待つのが基本パターン
  • 実測:1,000タスク同時実行で tokio::spawn は平均6.92ms、std::thread は平均270.4ms(約39倍の差)
  • features = ["full"] 指定時の tokio は 1.52.3 が解決され、初回ビルドは実測2分20秒かかった
  • 最新のRust言語機能が必要なら rustup で stable を入れるのが定石

目次

  1. 動作確認済み環境
  2. RustとCargoをUbuntuにインストールする
  3. Tokioの非同期ランタイムとは何か
  4. TokioをCargoプロジェクトに追加する
  5. 基本パターン:タスクの生成と待機
  6. 実践パターン
  7. 非同期 vs スレッド ベンチ結果(実測)
  8. Ubuntu 22.04 vs 24.04 Rust バージョン比較
  9. よくあるエラーと対処法
  10. まとめ

動作確認済み環境

注意

本記事のコマンドはすべて Ubuntu 24.04 LTS(Docker公式イメージ ubuntu:24.04、arm64ホスト)で実行・確認しています。Ubuntu 22.04でも大部分は同一ですが、一部パッケージバージョンが異なります(後述)。

  • OS: Ubuntu 24.04.4 LTS (Noble Numbat)
  • Rust: rustc 1.75.0apt install cargo build-essential でインストール)
  • Cargo: cargo 1.75.0
  • Tokio: 1.52.3features = ["full"] 指定時に解決された版)
  • 実行環境: Docker公式イメージ ubuntu:24.04(arm64 / Apple Silicon ホスト、2026-06-14 実測)
動作確認済み環境とビルド実測値(Ubuntu 24.04.4 LTS 実測)
動作確認済み環境とビルド実測値(Ubuntu 24.04.4 LTS 実測)

あとで詳しく見ますが、apt で入る Rust は 1.75.0、features = ["full"] で解決される tokio は 1.52.3でした。tokio とその依存クレート(tokio-macros 2.7.0・mio 1.2.1 など)を含めた初回の debug ビルドには、実測で2分20秒かかっています。「依存が少ないはず」と思って待っていると意外と長いので、心の準備をしておきましょう。

RustとCargoをUbuntuにインストールする

Ubuntu 24.04 LTSでは、apt から直接Rustコンパイラ(rustc)とパッケージマネージャ(cargo)をインストールできます。リンクに必要な build-essential(C/C++コンパイラなどのツール群)も一緒に入れましょう。




ubuntu@linuxlab: ~
$ apt-get update && apt-get install -y cargo build-essential

Setting up gcc (4:13.2.0-7ubuntu1) …
Setting up rustc (1.75.0+dfsg0ubuntu1-0ubuntu7.4) …
Setting up cargo (1.75.0+dfsg0ubuntu1-0ubuntu7.4) …
Setting up build-essential (12.10ubuntu1) …
$ rustc –version
rustc 1.75.0 (82e1608df 2023-12-21) (built from a source tarball)
$ cargo –version
cargo 1.75.0

インストール後、rustc --version1.75.0cargo --version1.75.0 と表示されれば完了です。これは Ubuntu 24.04 のパッケージリポジトリ(noble-updates/main)から取得した、Ubuntu公式のパッチ済みビルドです(パッケージ版は 1.75.0+dfsg0ubuntu1-0ubuntu7.4)。同時に gcc 13.2.0 も入り、Cのリンクが必要なクレートもビルドできるようになります。

最新版が必要な場合は rustup を使う

aptで入るRust 1.75.0はUbuntu公式のパッチ済みビルドです。async fn in trait の安定化(Rust 1.75)など比較的新しい機能は使えますが、それより新しい言語機能が必要な場合は rustup で最新の stable チャンネルを入れるのが正解です。

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Tokioの非同期ランタイムとは何か

Rustの async/await 構文は「非同期に動ける処理の書き方」を提供しますが、実際にタスクを動かすエンジン(ランタイム)は標準ライブラリに含まれていません。そこで登場するのがTokioです。

Tokioは次の3つの主要コンポーネントで構成されています。

  • スケジューラtokio::spawn で作られた非同期タスクを複数のワーカースレッドに分配する司令塔。work-stealingアルゴリズムで負荷を均等化します
  • I/Oドライバー:LinuxではOS標準の epoll を使い、数千の接続を少数のスレッドで効率的に監視します
  • タイマーホイールtokio::time::sleep などの時間処理をOSタイマーに頼らず内部で効率的に管理します
Tokio 非同期ランタイムのアーキテクチャ(概念図)
Tokio 非同期ランタイムのアーキテクチャ(概念図)

重要なのは、async fn を書いた時点ではまだ何も実行されていないという点です。.await したとき初めて、Tokioのランタイムに「このタスクを実行してほしい」と依頼が入ります。正直、最初はここが一番わかりにくいポイントです。

TokioをCargoプロジェクトに追加する

①新規プロジェクトを作成する

今回は tokiolab という名前のプロジェクトを作り、後で複数のサンプルを src/bin/ に置けるようにします。




ubuntu@linuxlab: ~
$ cargo new tokiolab
Created binary (application) `tokiolab` package
$ cd tokiolab

②Cargo.toml にTokioを追加する

Cargo.toml[dependencies] に Tokio を追加します。features = ["full"] は開発・学習段階で一番手軽な設定で、ネットワーク・タイマー・タスクなどすべての機能を有効にします(本番では必要な機能だけ選ぶと依存とビルド時間を減らせます)。ベンチ用に [profile.release] で最適化レベルも指定しておきます。




ubuntu@linuxlab: ~/tokiolab
$ cat Cargo.toml
[package]
name = “tokiolab”
version = “0.1.0”
edition = “2021”

[dependencies]
tokio = { version = “1”, features = [“full”] }

[profile.release]
opt-level = 3

依存を入れたら、cargo tree で実際に解決された Tokio のバージョンを確認できます。今回の環境では tokio v1.52.3 が解決されました。




ubuntu@linuxlab: ~/tokiolab
$ cargo tree | grep tokio
└── tokio v1.52.3
└── tokio-macros v2.7.0

基本パターン:タスクの生成と待機

①Hello Tokio: 最初の非同期プログラム

src/bin/hello.rs を次のように書きます。#[tokio::main] アトリビュートが、Tokioランタイムの起動と終了を自動的に処理してくれます。5つのタスクをそれぞれ100ミリ秒スリープさせ、全体の経過時間を測ります。




ubuntu@linuxlab: ~/tokiolab/src/bin/hello.rs
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
let start = std::time::Instant::now();
let handles: Vec<_> = (0..5).map(|i| {
tokio::spawn(async move {
sleep(Duration::from_millis(100)).await;
println!(“task {} done”, i);
})
}).collect();
for h in handles { h.await.unwrap(); }
println!(“elapsed: {:?}”, start.elapsed());
}

ビルドして実行してみましょう。初回ビルドは tokio と依存クレートのコンパイルで時間がかかります(実測:debug で2分20秒)。




ubuntu@linuxlab: ~/tokiolab
$ cargo build –bin hello
Compiling tokio-macros v2.7.0
Compiling tokio v1.52.3
Compiling tokiolab v0.1.0 (/work/project)
Finished dev [unoptimized + debuginfo] target(s) in 2m 20s
$ ./target/debug/hello
task 4 done
task 0 done
task 1 done
task 2 done
task 3 done
elapsed: 101.11125ms
Tokio Hello World と実践パターンの実行結果(ubuntu:24.04 Docker / tokio 1.52.3 実測)
Tokio Hello World と実践パターンの実行結果(ubuntu:24.04 Docker / tokio 1.52.3 実測)

注目してほしいのが経過時間です。5つのタスクがそれぞれ100ms待機しているのに、合計は101.11125ms(直列なら500ms)で終わっています。これがTokioの非同期タスクが並行実行される証拠です。また、完了順が 0,1,2,3,4 ではなく 4,0,1,2,3 とバラついているのも非同期の特性で、どのタスクが先に終わるかはスケジューラが決めます(実行ごとに変わります)。

実践パターン

ここからは src/bin/patterns.rs に4つの定番パターンをまとめ、実際の出力を確認していきます。実行結果は前掲のスクリーンショット(実測)にまとめてあります。

①複数タスクの並行実行と結果収集(join!)

複数の独立した処理を並行実行し、全部の結果が揃ってから次に進みたい場合は tokio::join! マクロが便利です。




ubuntu@linuxlab: ~/tokiolab/src/bin/patterns.rs
use tokio::time::{sleep, Duration};

async fn fetch_data(id: u32) -> String {
sleep(Duration::from_millis(100 * id as u64)).await;
format!(“data from {}”, id)
}

let (r1, r2, r3) = tokio::join!(
fetch_data(1), fetch_data(2), fetch_data(3),
);
// 実測出力:
// [join] data from 1, data from 2, data from 3 (elapsed 301.550833ms)

3つの fetch_data はそれぞれ100ms・200ms・300ms かかりますが、並行実行されるため合計は最長の約300ms(実測 301.550833ms)で完了します。直列なら 100+200+300=600ms かかる計算です。

②タイムアウトの設定(tokio::time::timeout)

外部APIへのリクエストなど、時間がかかる可能性がある処理には必ずタイムアウトを設定しましょう。tokio::time::timeout を使います。




ubuntu@linuxlab: ~/tokiolab/src/bin/patterns.rs
use tokio::time::{sleep, timeout, Duration};

let result = timeout(
Duration::from_millis(50),
sleep(Duration::from_millis(200)), // 200ms かかる処理
).await;
match result {
Ok(_) => println!(“[timeout] completed”),
Err(_) => println!(“[timeout] timed out after 50ms”),
}
// 実測出力: [timeout] timed out after 50ms

50msのタイムアウトに対して中の処理は200msかかるので、Err 側に分岐し [timeout] timed out after 50ms が出力されます。

③チャンネルでタスク間通信(mpsc)

Tokioには tokio::sync::mpsc(Multi-Producer Single-Consumer)チャンネルがあります。std::sync::mpsc と似た使い方ですが、非同期タスク間で安全にデータを渡せるのが特徴です。




ubuntu@linuxlab: ~/tokiolab/src/bin/patterns.rs
use tokio::sync::mpsc;

let (tx, mut rx) = mpsc::channel::<String>(32);
tokio::spawn(async move {
for i in 0..3 {
tx.send(format!(“msg {}”, i)).await.unwrap();
}
});
while let Some(msg) = rx.recv().await {
println!(“[mpsc] received: {}”, msg);
}
// 実測出力: received: msg 0 / msg 1 / msg 2

④ブロッキング処理をTokio内で扱う(spawn_blocking)

大量の数値計算やJSONの巨大パースなど、CPUを長時間占有するブロッキング処理をそのまま async fn の中に書くと、ワーカースレッドが詰まってしまいます。tokio::task::spawn_blocking を使うと、ブロッキング処理を専用のスレッドプールで実行できます。




ubuntu@linuxlab: ~/tokiolab/src/bin/patterns.rs
let sum = tokio::task::spawn_blocking(|| {
(0u64..10_000_000).sum::<u64>()
}).await.unwrap();
println!(“[spawn_blocking] sum = {}”, sum);
// 実測出力: [spawn_blocking] sum = 49999995000000

実際に cargo run --bin patterns を実行すると、上記4パターンが順番に動き、sum = 49999995000000 まで実測どおり出力されました(前掲スクリーンショット参照)。

非同期 vs スレッド ベンチ結果(実測)

「非同期はスレッドより軽い」とよく言われますが、本当にそうなのか実測してみましょう。src/bin/bench.rs で、1,000個のタスクを「Tokioの非同期タスク(tokio::spawn)」と「OSスレッド(std::thread::spawn)」の2方式で実行し、所要時間を比較します。各タスクは100マイクロ秒スリープするだけです。cargo build --release で最適化ビルドし、3回計測しました。

bench.rs の生出力(async vs thread 1,000タスク・3回計測・実測ターミナル)
bench.rs の生出力(async vs thread 1,000タスク・3回計測・実測ターミナル)

実行すると、機械可読な RESULT 行が出力されます。これがベンチの生データです。

1,000タスク実行 Tokio vs OSスレッド 平均比較(実測)
1,000タスク実行 Tokio vs OSスレッド 平均比較(実測)
実行方法 所要時間(3回計測) 平均 特徴
tokio::spawn(非同期) 4.96ms / 5.65ms / 10.17ms 6.92ms タスクをワーカースレッド上で多重化
std::thread::spawn(OSスレッド) 361.18ms / 227.53ms / 222.35ms 270.36ms 1,000個のOSスレッドを都度生成
1,000タスクの所要時間 3回分の生データ(実測)
1,000タスクの所要時間 3回分の生データ(実測)

平均で比べると tokio::spawn が std::thread の約39倍高速(270.36ms ÷ 6.92ms ≒ 39.0)という結果になりました。最も差が出た1回目では 361.18ms 対 4.96ms で約73倍です。理由はシンプルで、OSスレッドを1,000個生成するとスタック割当とカーネル呼び出しのコストがかかるのに対し、Tokioのタスクは少数のワーカースレッド上で多重化されるためほぼコストがかからないからです。I/O待機が多いWebサーバーやデータベースクライアントでは、この差はさらに顕著になります。

ちなみにこの計測を行ったコンテナのCPU性能(sysbench 1.0.20、2スレッド・各10秒・3回)も参考に載せておきます。平均4,593.80 events/sec でした。

実行環境のCPU性能 sysbench 3回(実測)
実行環境のCPU性能 sysbench 3回(実測)
著者アイコン
著者アイコン

注意したいのは、この「39倍」は大量のタスクを生成するケースの話だということ。CPUバウンドな処理(画像処理・暗号化など)ではOSスレッドや spawn_blocking の方が向いています。TokioのワーカースレッドはI/O待ちで詰まらない設計なので、CPU処理を長く占有すると他のタスクが影響を受けてしまうんですよね。

Ubuntu 22.04 vs 24.04 Rust バージョン比較

Ubuntu 22.04と24.04のaptリポジトリからインストールできるRustのバージョンを、両方のDockerイメージで実際に確認しました。

Ubuntu 22.04 vs 24.04 apt Rust バージョン比較(実測)
Ubuntu 22.04 vs 24.04 apt Rust バージョン比較(実測)

興味深いのは、Ubuntu 22.04 LTS(Jammy)のオリジナルのリポジトリ(jammy/main)にはRust 1.58.1しか入っていなかった点です。しかし jammy-updates のバックポートで1.75.0が提供されており、現在はどちらのLTSでも apt から同じ1.75.0が入手できます。apt-cache policy rustc で候補バージョンを確認できます。




ubuntu@linuxlab: ~ — apt-cache policy rustc(Ubuntu 22.04)
$ apt-cache policy rustc
rustc:
Installed: (none)
Candidate: 1.75.0+dfsg0ubuntu1~bpo0-0ubuntu0.22.04.1
Version table:
1.75.0+dfsg0ubuntu1~bpo0-0ubuntu0.22.04.1 500
500 …/jammy-updates/main arm64 Packages
1.58.1+dfsg1~ubuntu1-0ubuntu2 500
500 …/jammy/main arm64 Packages

一方 Ubuntu 24.04(Noble)は noble-updates/main の候補が 1.75.0+dfsg0ubuntu1-0ubuntu7.4 で、最初から1.75.0が入ります。つまり apt 経由なら、どちらのLTSを使っても結局 Rust 1.75.0 にたどり着く、というのが実測の結論です。

よくあるエラーと対処法

①error[E0752]: `main` function is not allowed to be `async`

Tokioのアトリビュートなしで async fn main() と書くと出るエラーです。#[tokio::main]async fn main() の直前に追加するか、Cargo.toml にTokioを追加し忘れていないか確認してください。




エラー例
error[E0752]: `main` function is not allowed to be `async`
–> src/main.rs:1:1
|
1 | async fn main() {
| ^^^^^^^^^^^^^^^^ `main` function is not allowed to be `async`
解決策: #[tokio::main] を async fn main() の直前に追加する

②cannot find function `spawn` in module `tokio`

Cargo.toml で tokio のフィーチャー指定を忘れているか、tokio 依存自体が未追加です。tokio = { version = "1", features = ["full"] } が正しく入っているか確認してください。

③Cargoのビルドに非常に時間がかかる

Tokioには多くの依存クレートがあるため、初回ビルドは数分かかることが普通です。今回の環境(arm64・Docker)でも、debugの初回ビルドに実測で2分20秒かかりました。2回目以降はキャッシュが効いて数秒で完了します。低スペックのVPS(1コア・1GB RAM)では初回ビルドにさらに時間がかかることがあります。

メモリ不足でビルドが落ちる場合

1GBのRAMしかないVPSではCargoのコンパイルがOOM Killerに落とされることがあります。RUSTFLAGS="-C codegen-units=1" cargo build でコンパイルの並列度を下げるか、スワップを追加することで回避できます。

まとめ

  • Ubuntu 24.04 LTS では apt install cargo build-essential で Rust 1.75.0 をインストールできる
  • TokioはRustの非同期処理を動かすランタイム。epollベースのI/Oドライバーとwork-stealingスケジューラで効率的に並行処理を実現する
  • Cargo.tomltokio = { version = "1", features = ["full"] } を追加すると tokio 1.52.3 が解決され、#[tokio::main] を付ければすぐ使える
  • 実測:1,000タスクの実行は tokio::spawn が平均6.92ms、std::thread が平均270.36ms で、約39倍の差がついた
  • CPUバウンドな重い処理は tokio::task::spawn_blocking で専用スレッドプールに回す
  • 初回ビルドは数分かかる(実測2分20秒)。最新のRust言語機能が必要なら rustup で最新stableを使う

Tokioを学ぶ次のステップとして、tokio::net::TcpListener を使ったTCPサーバーや、axum(Tokioベースの軽量Webフレームワーク)へ進むと、非同期の強みが実感できます。VPSの選び方は VPS比較記事 も参考にしてください。

自分のVPS上でRustのサーバーを動かしてみたい方は、まず環境構築から始めてみましょう。学習用なら月数ドルの小さなインスタンスで十分です。

コメント

タイトルとURLをコピーしました