はじめに
現代のアプリケーション、特にネットワークサービスでは、同時に多くの処理をこなす必要があります。例えば、Webサーバーは何千ものクライアントからのリクエストを同時に処理しなければなりません。従来の同期的な処理方法では、一つの処理が終わるまで次の処理に進めないため、リソースを効率的に使うことが難しい場面があります。
そこで登場するのが非同期処理です。Rustでは、async
/await
構文を使って、見た目は同期的でありながら、効率的な非同期処理を記述できます。これにより、I/O操作(ファイル読み書きやネットワーク通信など)で待機時間が発生しても、その間に他のタスクを進めることができるようになります。
このセクションでは、Rustのasync
/await
の基本的な概念と、非同期処理を実行するためのランタイムであるTokioの基本的な使い方について学んでいきます。
async
とは?
async
キーワードは、関数やブロックを非同期にするために使います。async fn
で定義された関数は、通常の関数とは異なり、呼び出された時点では実行されません。代わりに、Futureという特別な型を返します。
Future
トレイトは、将来のある時点で完了する可能性のある計算を表します。Future
は「遅延評価」であり、実行されるまで(具体的には.await
されるか、ランタイムに渡されるまで)は何もしません。これは、async fn
を呼び出すだけでは、その中のコードがすぐに実行されるわけではない、という重要な点です。
await
とは?
.await
演算子は、Future
の完了を待機するために使われます。async
関数やasync
ブロックの内部でのみ使用できます。
.await
が重要なのは、Future
がまだ完了していない(例えば、ネットワークからの応答を待っている)場合に、現在のタスクの実行を一時停止し、他のタスクに実行を譲る点です。これにより、スレッドをブロックすることなく、効率的にリソースを利用できます。Future
が完了したら、中断した箇所から処理を再開します。
上記の例では、learn_song()
の完了を.await
で待ってから、その結果を使ってsing_song()
を呼び出しています。.await
を使わない場合(例えば同期的なblock_on
を使う場合)、スレッド全体がブロックされてしまい、他のタスク(例えばdance
)を同時に実行できなくなります。
非同期ランタイムとは?
async fn
が返すFuture
は、それ自体を実行する能力を持ちません。Future
を実行し、進捗を管理する役割を担うのが非同期ランタイム(またはエグゼキュータ)です。
ランタイムは以下のような機能を提供します:
- タスクスケジューラ: 多数の
Future
(タスク)を管理し、どれを実行するかを決定します。 - リアクター: OSからのI/Oイベント(ネットワーク、ファイル、タイマーなど)を監視し、関連するタスクを再開させます。
- スレッドプール (オプション): 複数のCPUコアを活用するために、タスクを複数のスレッドで実行します。
Rustの標準ライブラリには非同期ランタイムは含まれていません。これは、ユースケースに応じて最適なランタイムを選択できるようにするためです。代表的な非同期ランタイムには以下のようなものがあります。
- Tokio: 最も広く使われている、高機能でパフォーマンス重視のランタイム。ネットワークアプリケーション開発に適しています。
- async-std: 標準ライブラリのAPIに対応する非同期版を提供することを目指すランタイム。
- smol: シンプルで軽量なランタイム。
この学習サイトでは、最も普及しているTokioに焦点を当てて解説します。
Tokioの基本
Tokioは、Rustで信頼性が高く、高速な非同期アプリケーションを構築するためのランタイムです。イベント駆動型でノンブロッキングなI/Oを提供し、TCP/UDP、タイマー、ファイルシステム操作、プロセス管理など、非同期プログラミングに必要な多くの機能を提供します。
セットアップ
まず、Cargo.toml
にTokioを追加します。多くの機能が含まれているため、必要な機能だけを選択することも可能ですが、初学者はfull
フィーチャーフラグを有効にするのが簡単です。
現在のTokioの最新バージョンや推奨されるMSRV(Minimum Supported Rust Version)については、crates.ioやTokioのGitHubリポジトリを確認してください。
#[tokio::main]
アトリビュート
Tokioを使って非同期コードを実行する最も簡単な方法は、main
関数に#[tokio::main]
アトリビュートを付けることです。このマクロは、async fn main()
を通常の同期的なfn main()
に変換し、内部でTokioランタイムを初期化してasync main
関数を実行します。
このコードを実行すると、Tokioランタイムが起動し、main
関数内の非同期コードが実行されます。sleep
関数は非同期な待機を行い、その間スレッドはブロックされません。
#[tokio::main]
は、デフォルトではマルチスレッドのランタイムを起動します。シングルスレッドで実行したい場合などは、マクロに引数を渡して設定を変更できます。
タスクのスポーン: tokio::spawn
複数の非同期処理を並行に実行したい場合、tokio::spawn
を使います。spawn
は新しい非同期タスク(軽量なグリーンスレッドのようなもの)を生成し、Tokioランタイムのスケジューラに実行を依頼します。spawn
されたタスクは、呼び出し元のタスクとは独立して実行されます。
この例では、task_one
とtask_two
が並行して実行されます。task_two
の方が待機時間が短いため、先に終了するはずです。spawn
はJoinHandle
を返し、これを使ってタスクの完了を待ったり、タスクの返り値を取得したりできます。
重要な注意点として、tokio::spawn
で起動されるタスクはSend
トレイトを実装し、かつ'static
ライフタイムを持つ必要があります。これは、タスクが別のスレッドに移動される可能性があるためです。多くの場合、async move { ... }
ブロックを使って、必要な変数の所有権をタスク内に移動させます。
async/await
とTokioを使った非同期I/O
非同期処理が特に役立つのは、ファイルI/Oやネットワーク通信のような、時間がかかる可能性のあるI/O操作です。Tokioはこれらのための非同期APIを提供しています。
tokio::fs
: 非同期ファイルシステム操作tokio::net
: 非同期TCP、UDP、Unixソケットtokio::process
: 非同期プロセス管理
これらのAPIを使うことで、I/O操作の待機中にスレッドをブロックすることなく、他のタスクを実行できます。
この例では、File::open
やread_to_string
が非同期に実行されます。もしexample.txt
が非常に大きなファイルだったとしても、読み込み中に他のタスク(もしあれば)が実行される可能性があります。
注意点とまとめ
- ランタイムが必須:
async fn
やFuture
を実行するには、Tokioのような非同期ランタイムが必要です。#[tokio::main]
を使うのが最も簡単な方法です。 .await
の場所:.await
はasync
関数またはasync
ブロックの中でのみ使用できます。- ブロッキング操作の回避:
async
なコンテキスト内で、標準ライブラリの同期的なI/O関数(std::fs::read
など)や時間のかかるCPU処理を直接呼び出すと、ランタイムのスレッドをブロックしてしまい、非同期処理の利点が失われます。このような場合は、tokio::task::spawn_blocking
を使って別のスレッドで実行することを検討してください。 Send
と'static
:tokio::spawn
でタスクを生成する場合、タスク(Future)は通常Send + 'static
である必要があります。async move
ブロックがよく使われます。- 関数カラーリング:
async
関数はasync
なコンテキストからしか直接.await
できません。同期関数から非同期関数を呼び出すには、ランタイムの機能(block_on
など)を使う必要がありますが、注意が必要です。
async/await
とTokioは、Rustで効率的でスケーラブルなネットワークアプリケーションなどを構築するための強力なツールです。最初は少し複雑に感じるかもしれませんが、Future
、async
/await
、そしてランタイムの役割を理解することが重要です。
次のステップでは、より高度なTokioの機能(チャネル、同期プリミティブ、ストリームなど)や、非同期コードのエラーハンドリングについて学んでいきましょう!