非同期処理のナビゲーション - async-stdとTokioの徹底比較
Emily Parker
Product Engineer · Leapcell

はじめに
Rustの強力な所有権および借用システムは、パフォーマンスと安全性への注力と相まって、堅牢で効率的なアプリケーションを開発するための説得力のある選択肢となっています。現代のソフトウェアにおける重要な側面は並行性であり、Rustではasync
/await
キーワード(Rust 1.39で導入)によって大幅に推進された非同期プログラミングが、高性能なノンブロッキングI/O操作を記述するための事実上の標準となっています。このパラダイムシフトは、スケーラブルなネットワークサービス、Webアプリケーション、その他のI/Oバウンドシステムを構築するための新しい可能性の扉を開きました。
しかし、async
/await
構文自体は完全なソリューションを提供するものではなく、これらのFuture
を実行するための「非同期ランタイム」が必要です。Rustエコシステムでは、async-std
とTokioの2つの著名な非同期ランタイムがリーダーとして登場しました。どちらも開発者が効率的な非同期コードを書くことを可能にしますが、問題へのアプローチはわずかに異なり、プロジェクト固有のニーズに応じて、異なる利点と欠点を提供します。これらの違いを理解することは、ランタイムの選択が開発体験、パフォーマンス特性、および専門ライブラリの可用性に大きく影響する可能性があるため、非同期の旅に乗り出すすべてのRust開発者にとって最重要です。この記事は、これらの2つの巨人を解明し、選択プロセスをガイドするための包括的な比較を提供することを目指しています。
非同期Rustランタイムの解明
async-std
とTokioの具体例に入る前に、非同期Rustプログラミングのいくつかのコアコンセプトを理解することが不可欠です。
Future: Rustでは、Future
は値を生み出す可能性のある非同期計算を表すトレイトです。JavaScriptのPromiseやC#のTaskに似ています。Future
は「怠惰」であり、エグゼキュータによってポーリングされない限り何も行いません。
Executor: エグゼキュータは、Future
をポーリングし、その状態を進める責任があります。Future
がI/Oイベント(例:ネットワークソケットへのデータの到着)を待つ必要がある場合、エグゼキュータにPoll::Pending
を返します。エグゼキュータは、イベントが準備完了になったときに通知されるように自身を登録し、その後Future
を再度ポーリングできます。このノンブロッキングの性質が、単一のスレッドが複数の並行操作を処理することを可能にします。
Reactor (Event Loop): Reactorは、I/Oイベントを監視する非同期ランタイムのコアコンポーネントです。これはしばしばイベントループとして実装され、オペレーティングシステムのI/O機能(Linuxのepoll
、macOS/BSDのkqueue
、またはWindowsのIOCP
など)から新しいイベント(例:データ利用可能、接続クローズ)を継続的に待ちます。イベントが発生すると、Reactorは適切なFuture
またはタスクに実行を再開するよう通知します。
それでは、async-std
とTokioを探ってみましょう。
Tokio: パフォーマンス指向の強力なランタイム
Tokioは、特に高性能ネットワークサービスにおいて、非同期Rust開発の事実上の標準と見なされています。マルチスレッドスケジューラ、I/Oドライバ、およびさまざまなプロトコルやユーティリティのための広範な関連クレートのエコシステムを含む、非同期アプリケーションの包括的なビルディングブロックを提供します。
主要な原則と機能:
- マルチスレッドスケジューラ: Tokioのデフォルトスケジューラは、マルチコアシステムでの高スループットを目的として設計されています。ワークスティーリングアルゴリズムを使用しており、アイドル状態のワーカー・スレッドがビジーなスレッドからタスクを「盗む」ことで、CPU利用率を最大化します。
- レイヤードデザイン: Tokioはモジュラーでレイヤードなアーキテクチャで構築されています。コア
tokio
クレートはランタイムを提供し、tokio-util
、hyper
、tonic
などの個別のクレートは、より高レベルの抽象化とプロトコル実装を提供します。 - パフォーマンス重視: Tokioは生のパフォーマンスを優先します。その内部は一般的なネットワークプログラミングパターンに対して高度に最適化されており、最小限のレイテンシと最大の可能なスループットを要求するアプリケーションに強力な選択肢となります。
- 豊富なエコシステム: その人気により、Tokioは巨大なエコシステムを誇っています。Rustコミュニティの多くのライブラリ、特にネットワーキング、データベース、Webフレームワークに関連するものは、Tokio上に構築されているか、Tokioとうまく統合されています。
Tokioを使用したシンプルなTCPエコーサーバーの例
use tokio::net::TcpListener; use tokio::io::{AsyncReadExt, AsyncWriteExt}; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let listener = TcpListener::bind("127.0.0.1:8080").await?; println!("Tokio Echo Server listening on 127.0.0.1:8080"); loop { let (mut socket, peer_addr) = listener.accept().await?; println!("Accepted connection from: {}", peer_addr); tokio::spawn(async move { let mut buf = vec![0; 1024]; loop { match socket.read(&mut buf).await { Ok(0) => break, // Connection closed Ok(n) => { // Echo the data back if socket.write_all(&buf[..n]).await.is_err() { break; } } Err(_) => break, // Error } } println!("Connection from {} closed.", peer_addr); }); } }
この例では、@tokio::main
はTokioランタイムをセットアップし、私たちのmain
関数をそのコンテキストで実行するマクロです。tokio::spawn
は並行して実行される新しい非同期タスクを作成します。tokio::io
のAsyncReadExt
とAsyncWriteExt
の使用に注目してください。これらはノンブロッキングI/O操作を提供します。
async-std: シンプルさ第一のアプローチ
async-std
は「非同期プログラミングのための標準ライブラリ」を提供することを目指しています。その設計思想は、馴染みのあるstd
ライブラリAPIを模倣することを中心に据えており、非同期コードへの移行をより自然で威圧感の少ないものにしています。
主要な原則と機能:
- 標準ライブラリAPIの同等性:
async-std
は、可能な限りI/Oおよび並行性のための標準ライブラリのAPIをミラーリングしようとします。例えば、async_std::fs::File
はstd::fs::File
と同様のメソッドを持っていますが、それらはasync
です。 - シングル・スレッドまたはマルチ・スレッド: マルチ・スレッド・エグゼキュータを提供しますが、
async-std
の設計は、よりシンプルなシングル・スレッド・モデルまたは小さなスレッド・プールの恩恵を受けることができるアプリケーションでしばしば輝きます。その並行性モデルを推論するのは一般的に容易です。 - エルゴノミクスとシンプルさ:
async-std
は使いやすさと学習曲線の低さを優先します。そのAPIは、標準ライブラリに慣れている人には非常に慣用的なRustのように感じられます。 surf
Webフレームワーク:async-std
は、async-std
のために設計された人気のWebフレームワークであるsurf
と緊密に統合されており、Web開発のための合理化された体験を提供します。async-graphql
およびtide
の基盤:async-graphql
およびtide
Webフレームワークのようなプロジェクトはasync-std
上に構築されており、それぞれのドメインに堅牢なツールを提供します。
async-stdを使用したシンプルなTCPエコーサーバーの例
use async_std::net::TcpListener; use async_std::io::{ReadExt, WriteExt}; use async_std::task; // task::spawnのためのインポートを修正 #[async_std::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let listener = TcpListener::bind("127.0.0.1:8080").await?; println!("async-std Echo Server listening on 127.0.0.1:8080"); loop { let (mut stream, peer_addr) = listener.accept().await?; println!("Accepted connection from: {}", peer_addr); task::spawn(async move { // task::spawnを使用 let mut buf = vec![0; 1024]; loop { match stream.read(&mut buf).await { Ok(0) => break, // Connection closed Ok(n) => { // Echo the data back if stream.write_all(&buf[..n]).await.is_err() { break; } } Err(_) => break, // Error } } println!("Connection from {} closed.", peer_addr); }); } }
ここでは、@async_std::main
はasync-std
ランタイムを初期化します。task::spawn
は並列タスクを作成するために使用されます。async_std::net::TcpListener
とasync_std::io::{ReadExt, WriteExt}
が、構造と命名においてそれらのstd
の対数と直接同等であることを確認してください。
比較と選択の考慮事項
特徴/側面 | Tokio | async-std |
---|---|---|
設計思想 | パフォーマンス重視、ベアメタル制御 | シンプルさ第一、std ライブラリの同等性 |
エグゼキュータ・モデル | マルチスレッド、ワークスティーリング(デフォルト) | シングル・スレッドまたはマルチ・スレッド |
エコシステムとライブラリ | 非常に豊富、広範、業界標準 | 成長中、特定ニッチに最適 |
APIスタイル | より明示的、時に冗長 | std ライブラリ風、よりエルゴノミック |
一般的なユースケース | 高性能サーバー、RPC、データベース | Webサービス(surf, tide)、シンプルなアプリ |
学習曲線 | 複雑なシナリオでは急勾配 | より穏やか、特にstd ユーザーにとって |
リソース使用量 | 一般的に高い初期メモリオーバーヘッド | 一般的に低い初期メモリオーバーヘッド |
Tokioを選択する場合:
- 高いパフォーマンス要件: アプリケーションが可能な限り最高のスループットと最小限のレイテンシ、特にネットワーク集約型のワークロードを要求する場合、Tokioの最適化されたスケジューラとI/Oスタックが最良の選択肢となる可能性が高いです。
- 大規模で複雑なアプリケーション: エンタープライズグレードのサービス、マイクロサービスアーキテクチャ、または高度な並行性プリミティブを必要とするシステムの場合、Tokioの包括的なツールのセットと実績のある性質が強固な基盤を提供します。
- 豊富なエコシステムを活用: プロジェクトが3rdパーティライブラリ(例:gRPCクライアント/サーバー、高度なHTTPクライアント、データベースドライバ)に大きく依存している場合、Tokioのエコシステム内でより広範で成熟したサポートを見つけることがよくあります。
async-stdを選択する場合:
- シンプルさと使いやすさ: 非同期Rustに慣れていない開発者や、生のパフォーマンスよりも開発速度とコードの明瞭さが優先されるプロジェクトでは、
async-std
はよりアクセスしやすいAPIを提供します。 std
ライブラリへの親しみ: 同期Rust標準ライブラリに密接に似た非同期プログラミングモデルを好む場合、async-std
は非常に自然に感じられるでしょう。- Webサービス(surf/tide):
surf
またはtide
を使用してWebアプリケーションを構築する予定がある場合、async-std
はネイティブで最も統合された選択肢です。 - 小規模なアプリケーションまたは特定のドメイン: 小規模なユーティリティ、スクリプト、または最高のパフォーマンスが絶対的な最優先事項ではないが
async
I/Oが望ましいアプリケーションの場合、async-std
は優れた選択肢となり得ます。
また、futures::io::AsyncRead
やfutures::io::AsyncWrite
のようなトレイトのおかげで、Rustの非同期エコシステムがランタイムに依存しないライブラリにますます焦点を当てていることは注目に値します。これにより、一部のライブラリはどちらのランタイムでも動作し、厳密な結びつきを減らすことができます。しかし、コアI/O抽象化とエグゼキュータについては、依然として選択を行う必要があります。
結論
async-std
とTokioはどちらもRustにとって非常に強力で成熟した非同期ランタイムであり、それぞれが明確な設計原則で独自のニッチを切り開いています。Tokioは、要求の厳しい複雑なネットワークシステムに理想的な、高性能で機能豊富なワークホースとして位置づけられています。一方、async-std
は、エルゴノミックでstd
ライクな体験を提供し、幅広いアプリケーションにおけるシンプルさと使いやすさに優れています。最終的な選択は、プロジェクト固有の要件、各ランタイムへのチームの精通度、および遭遇する可能性のある特定のエコシステム・ニーズに最終的に依存します。