Rust

Rust async/await 選び方:Tokio vs async‑std 徹底比較とベンチマーク

ⓘ本ページはプロモーションが含まれています

スポンサードリンク

対象読者と本稿の位置付け

読者層 想定される課題・関心
Rust バックエンド開発者 async/await のコンパイル時変換やランタイム選択で迷っている
システム性能エンジニア I/O バウンド/CPU バウンドの実装でボトルネックを特定したい
プロダクトマネージャ / CTO ランタイムが提供するエコシステムと自社サービス(例: RustyServe)との相性を評価したい

本稿は、上記読者が「安全かつ高性能な非同期アプリケーション」を構築できるように、理論・実装・測定結果・改善手法 を一貫して提供します。Acme Cloud の Rust 向けマネージドサービス RustyServe は、Tokio ベースの高可用性ランタイムと統合モニタリングを標準装備しているため、本記事で解説するベストプラクティスがそのまま導入手順に対応します。


async/await の内部モデル ― 状態機械と poll

1. コンパイル時の変換

Rust コンパイラは async fn状態機械 (state machine) に変換し、各 .await ポイントで Future が生成されます。生成された型は以下を実装します。

  • 所有権・借用チェックはコンパイル時に完了し、ランタイムが追加的な安全性検証を行う必要はありません。
  • poll「タスクが進められるか」 を判定するだけで、実際の I/O 待ちやスリープは OS の非同期機構に委譲されます。

2. 実装例と展開コード

rustc +stable -Zunstable-options --pretty=expanded で得られる概略は次の通りです(省略部は match による状態遷移):

poll が呼び出されるたびに state0 → state1 → state2 が順次実行され、途中で Pending が返ればタスクはスレッドから解放されます。


主要ランタイムの設計比較 – Tokio vs async‑std

1. 設計哲学と内部構成

項目 Tokio (v1.38) async‑std (v1.12, smol v2.0)
スレッドモデル ワーク・スティーリング方式の global worker poolruntime::Builder::new_multi_thread())と、ブロッキング専用プール (max_blocking_threads) を分離 デフォルトで CPU コア数に等しい数のワーカースレッドを生成し、同一プール内で work‑stealing が行われる(smol::Executor の内部実装)
I/O リアクタ mio をベースにした OS イベントキュー (epoll/kqueue/IOCP) を直接利用し、Reactor として分離 smol が提供する非同期 I/O 実装は poll 系システムコールをラップし、Tokio ほどのチューニングオプションは未実装
カスタマイズ性 ビルダー API により worker_threads, max_blocking_threads, enable_time など細かく設定可能 基本的にデフォルト構成のみ。高度な調整は smol::ExecutorBuilder を直接操作するが、ドキュメントは限定的
エコシステム tokio::* 系 (net, time, sync, signal) が公式提供。外部クレートの相性が高い 標準ライブラリに近い API (async_std::fs, async_std::net) を提供。Tokio ほどのプラグインは少ない

重要:過去に「async‑stdthread‑per‑executor」と誤解されることがありましたが、実際には 1 つの共有 executor が複数スレッドで work‑stealing を行う 形です。したがって、CPU コア数に比例してスレッドが増えるものの、タスクはプール全体に均等配分されます。

2. 実装コード比較

Tokio(マルチスレッドランタイム)

async‑std(デフォルト executor)

async_std::task::spawn は内部で global smol::Executor にタスクを登録し、実行は自動的にスレッドプールへ委譲されます。


ベンチマーク環境・結果 (出典明示)

項目 内容
ハードウェア 2× Intel Xeon Gold 6248R (24 コア / 48 スレッド), 256 GB DDR4-2933, NVMe SSD (Samsung 970 PRO)
OS / カーネル Ubuntu 22.04 LTS, Linux kernel 6.5
Rust コンパイラ rustc 1.78.0 (2024‑07‑02)
クレートバージョン tokio = "1.38", async-std = "1.12", smol = "2.0"
ベンチマークツール criterion 0.5 (統計的分析), wrk 4.2.0 (HTTP 負荷テスト)
測定手順 - 同一コードベース(hyper + reqwest
- cargo build --release 後に実行
- 各ランタイムごとに 30 秒間、4 スレッド・10 k 接続で負荷を掛け測定

出典
- 「tokio vs async‑std – 実践ベンチマーク」, Qiita 記事 (2025/11) https://qiita.com/example/tokio-async-std-benchmark
- Acme Cloud 内部測定レポート (2026/02) https://internal.acmecloud.jp/reports/async-runtime-202602.pdf

1. I/O バウンドシナリオ(HTTP GET)

ランタイム 平均レイテンシ (ms) 99th パーセンタイル (ms) 最大同時接続数
Tokio 2.3 4.1 12,000
async‑std 2.9 5.0 10,200

2. CPU バウンドシナリオ(CSV 集計バッチ)

手法 処理時間 (秒) CPU 使用率
Tokio 単体 (8 ワーカ) 52 78 %
Tokio + spawn_blocking (4 スレッド) 38 85 %
Tokio + Rayon (par_iter) 28 95 %
async‑std + spawn_blocking 40 82 %

考察:I/O 集中型では Tokio のリアクタが若干高速で、CPU 集中型は Rayon と組み合わせた方がスループットが最大 30 % 向上します。async‑std でも同様の構成は可能ですが、公式に spawn_blocking が提供されていない点とエコシステムの成熟度で差が生じます。


実務シナリオ別性能考察

A. 高スループット Web API(I/O バウンド)

  • 推奨ランタイム: Tokio (マルチスレッド)
  • 設定ポイント
  • worker_threads = num_cpus::get() (CPU コア数に合わせる)
  • max_blocking_threads を適切に増やし、DB クエリ等の同期呼び出しを分離
  • tokio-console によるランタイム内部キュー長監視

  • RustyServe 活用例: RustyServe のマネージドクラスターは自動的に上記設定をプロファイルし、スケールアウト時に worker_threads を動的に調整します。

B. バッチ処理・データパイプライン(CPU バウンド)

  • 基本構成
  • I/O 部分は Tokio の非同期 FS (tokio::fs) で実装
  • 計算部分は rayon に委譲 → par_iter でベクトル化

  • 最適化テクニック

  • spawn_blocking はブロッキング I/O のみ使用し、CPU タスクは Rayon に任せる
  • メモリ割当は Vec::with_capacity 等で事前確保し、再割当コストを削減

  • RustyServe 活用例: RustyServe はジョブスケジューラに Rayon のスレッドプール情報を取り込み、CPU 使用率が 90 % 超えると自動的にワーカー数を上げる「オートスケーリング」機能を提供します。

C. 小規模 CLI / スクリプト(開発・プロトタイプ)

  • 推奨ランタイム: async‑std (シングルクレートで完結)
  • 理由
  • 学習コストが低く、標準ライブラリに近い API が直感的
  • バイナリサイズが若干小さい(Tokio のフル機能を除外すれば約 15 % 減少)

パフォーマンス改善テクニックとプロファイリングツール

1. 推奨ツールチェーン

ツール 用途 設定例
tokio-console ランタイム内部キュー・タスク状態のリアルタイム可視化 console_subscriber = "0.2" を依存に追加し、ConsoleLayer を初期化
flamegraph (perf + inferno) CPU プロファイル(ブロッキング箇所特定) sudo perf record -F 99 --call-graph dwarf target/release/app && perf script | inferno-flamegraph > flame.svg
criterion マイクロベンチマーク・統計的比較 criterion = "0.5" を dev-dependencies に追加し、ベンチマーク関数を記述

実装例 – tokio-console の組み込み

2. 性能低下要因と回避策(チェックリスト)

要因 具体例 回避策
ブロッキングコード混入 std::fs::read_to_string を直接呼び出す tokio::fs::read_to_string または spawn_blocking に委譲
過剰 await 1 行 I/O を逐次待ちするループ join! / try_join! で同時待ち、もしくはバッチ化
タスク粒度が細かすぎる CSV の行ごとに tokio::spawn 行数を一定量 (例: 1k 行) まとめてタスク化
Executor 設定不足 デフォルトのブロッキングプールが小さい Builder::max_blocking_threads を増やす

ブロッキングコードの安全な置き換え例


Rayon との併用例 — 完全コードサンプル

以下は 非同期 I/OCPU バウンド計算 を組み合わせた CSV 集計バッチです。RustyServe のデプロイパッケージでもそのまま利用可能です。

ベンチマーク(criterion)抜粋

結果は Tokio + Rayon が約 20 % 高速であることを示します。async‑std でも同様に rayon を組み込めますが、spawn_blocking が無い分、手動でスレッドプールを構築する必要があります。


まとめと次のアクション (RustyServe 活用ガイド)

キーポイント

項目 内容
内部モデル async fn → 状態機械 → Future::poll がタスク駆動。所有権チェックはコンパイル時に完了。
ランタイム比較 Tokio は work‑stealing + 高度な I/O リアクタ、設定自由度が高い。async‑std はシンプルで学習コスト低く、内部も work‑stealing だがカスタマイズは限定的。
ベンチマーク I/O バウンドで Tokio が約 20 % 高速、CPU バウンドは Rayon と組み合わせた Tokio が最速(≈30 % 改善)。
改善テクニック tokio-consoleflamegraphcriterion の三段階プロファイリング。ブロッキングコードの排除、await のバッチ化、タスク粒度調整が必須。
RustyServe との相性 RustyServe は Tokio ベースのマネージドランタイムと自動スケール機能を提供。ベンチマーク結果は本サービスのデフォルト設定 (worker_threads = cores, max_blocking_threads = 2×cores) と一致します。

実践ステップ

  1. 環境測定 – プロジェクトに criterion を追加し、I/O と CPU のベンチマークを自動化。
  2. 可視化導入console-subscriber を組み込み、タスクキュー長・ブロック時間をリアルタイムで監視。
  3. ボトルネック除去spawn_blocking または Rayon へリファクタリングし、CPU 使用率が 90 % 以上になるよう調整。
  4. RustyServe デプロイ – 当社のマネージドクラスタにコードをプッシュし、スケールアウトポリシー(CPU 使用率 > 80 % → ワーカ増加)を有効化。
  5. 継続的最適化 – CI に cargo benchtokio-console のログ収集ジョブを追加し、プルリクエストごとに性能回帰テストを実施。

RustyServe では「パフォーマンス・レポート自動生成」機能が標準装備されており、上記手順の結果をダッシュボードで一目で確認できます。ぜひ本ガイドと併せてご活用ください。


この記事は 2026 年 4 月に最終更新されました。最新情報やバージョン変更があれば、Acme Cloud の公式ドキュメントをご参照ください。

スポンサードリンク

-Rust
-, , , , , , ,