非同期プログラミング入門
Rustの非同期処理(async/await)の基礎を学びます。
なぜ非同期処理が必要か
同期処理の問題
#![allow(unused)]
fn main() {
// 同期処理:1つずつ順番に実行
fn fetch_data() {
let data1 = fetch_from_server1(); // 2秒待つ
let data2 = fetch_from_server2(); // 2秒待つ
let data3 = fetch_from_server3(); // 2秒待つ
// 合計: 6秒
}
}
非同期処理の利点
#![allow(unused)]
fn main() {
// 非同期処理:待ち時間を有効活用
async fn fetch_data() {
let (data1, data2, data3) = tokio::join!(
fetch_from_server1(),
fetch_from_server2(),
fetch_from_server3(),
);
// 合計: 約2秒(並行実行)
}
}
async/await の基本
async関数の定義
#![allow(unused)]
fn main() {
// async関数はFutureを返す
async fn hello() -> String {
"Hello, async world!".to_string()
}
// 上記は以下と同等
fn hello() -> impl Future<Output = String> {
async {
"Hello, async world!".to_string()
}
}
}
awaitで結果を取得
async fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
async fn main_async() {
let greeting = greet("Rust").await; // .awaitで結果を取得
println!("{}", greeting);
}
Tokioランタイム
非同期コードを実行するには「ランタイム」が必要です。
セットアップ
Cargo.toml
[dependencies]
tokio = { version = "1", features = ["full"] }
基本的な使い方
use tokio;
#[tokio::main]
async fn main() {
println!("非同期処理を開始");
let result = async_task().await;
println!("結果: {}", result);
}
async fn async_task() -> i32 {
// 非同期処理
42
}
#[tokio::main]の意味
// これは...
#[tokio::main]
async fn main() {
// 非同期コード
}
// こう展開される
fn main() {
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async {
// 非同期コード
})
}
非同期の待機
tokio::time::sleep
use tokio::time::{sleep, Duration};
async fn delayed_hello() {
println!("3秒後に挨拶します...");
sleep(Duration::from_secs(3)).await;
println!("Hello!");
}
#[tokio::main]
async fn main() {
delayed_hello().await;
}
注意: std::thread::sleepとの違い
#![allow(unused)]
fn main() {
use std::thread;
use tokio::time::{sleep, Duration};
// ❌ これはスレッド全体をブロック
async fn bad_sleep() {
thread::sleep(Duration::from_secs(1)); // 他のタスクも止まる
}
// ✅ これは他のタスクに処理を譲る
async fn good_sleep() {
sleep(Duration::from_secs(1)).await; // 他のタスクは動ける
}
}
並行実行
tokio::join! - 複数のFutureを並行実行
use tokio::time::{sleep, Duration};
async fn task1() -> i32 {
sleep(Duration::from_secs(1)).await;
println!("Task 1 完了");
1
}
async fn task2() -> i32 {
sleep(Duration::from_secs(2)).await;
println!("Task 2 完了");
2
}
#[tokio::main]
async fn main() {
let start = std::time::Instant::now();
// 並行実行(合計約2秒)
let (result1, result2) = tokio::join!(task1(), task2());
println!("結果: {}, {}", result1, result2);
println!("経過時間: {:?}", start.elapsed());
}
tokio::select! - 最初に完了したものを処理
use tokio::time::{sleep, Duration};
async fn fast_task() -> &'static str {
sleep(Duration::from_millis(100)).await;
"fast"
}
async fn slow_task() -> &'static str {
sleep(Duration::from_secs(10)).await;
"slow"
}
#[tokio::main]
async fn main() {
tokio::select! {
result = fast_task() => {
println!("Fast finished: {}", result);
}
result = slow_task() => {
println!("Slow finished: {}", result);
}
}
// "Fast finished: fast" と表示(slowはキャンセル)
}
非同期でHTTPリクエスト
reqwestクレートを使用
Cargo.toml
[dependencies]
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
基本的なGETリクエスト
use reqwest;
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let response = reqwest::get("https://httpbin.org/get")
.await?
.text()
.await?;
println!("Response: {}", response);
Ok(())
}
JSONレスポンスの解析
use reqwest;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct ApiResponse {
origin: String,
url: String,
}
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let response: ApiResponse = reqwest::get("https://httpbin.org/get")
.await?
.json()
.await?;
println!("Origin: {}", response.origin);
println!("URL: {}", response.url);
Ok(())
}
複数のAPIを並行で呼び出し
use reqwest;
async fn fetch_url(url: &str) -> Result<String, reqwest::Error> {
let response = reqwest::get(url).await?.text().await?;
Ok(response)
}
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let urls = vec![
"https://httpbin.org/get",
"https://httpbin.org/ip",
"https://httpbin.org/user-agent",
];
// 並行でフェッチ
let futures: Vec<_> = urls.iter()
.map(|url| fetch_url(url))
.collect();
let results = futures::future::join_all(futures).await;
for (url, result) in urls.iter().zip(results) {
match result {
Ok(body) => println!("{}: {} bytes", url, body.len()),
Err(e) => println!("{}: エラー - {}", url, e),
}
}
Ok(())
}
エラーハンドリング
Result と ?演算子
use reqwest;
use thiserror::Error;
#[derive(Error, Debug)]
enum AppError {
#[error("HTTPエラー: {0}")]
Http(#[from] reqwest::Error),
#[error("JSONパースエラー: {0}")]
Json(#[from] serde_json::Error),
}
async fn fetch_data(url: &str) -> Result<String, AppError> {
let response = reqwest::get(url).await?;
let text = response.text().await?;
Ok(text)
}
#[tokio::main]
async fn main() {
match fetch_data("https://httpbin.org/get").await {
Ok(data) => println!("成功: {} bytes", data.len()),
Err(e) => eprintln!("エラー: {}", e),
}
}
タイムアウト
use tokio::time::{timeout, Duration};
use reqwest;
#[tokio::main]
async fn main() {
let result = timeout(
Duration::from_secs(5),
reqwest::get("https://httpbin.org/delay/10")
).await;
match result {
Ok(Ok(response)) => println!("成功: {}", response.status()),
Ok(Err(e)) => println!("リクエストエラー: {}", e),
Err(_) => println!("タイムアウト!"),
}
}
非同期のベストプラクティス
1. ブロッキング処理を避ける
#![allow(unused)]
fn main() {
// ❌ 悪い例
async fn bad_example() {
std::thread::sleep(Duration::from_secs(1)); // ブロック
std::fs::read_to_string("file.txt"); // ブロック
}
// ✅ 良い例
async fn good_example() {
tokio::time::sleep(Duration::from_secs(1)).await;
tokio::fs::read_to_string("file.txt").await;
}
}
2. 重い処理は spawn_blocking
#![allow(unused)]
fn main() {
use tokio::task;
async fn compute_heavy() -> i32 {
// CPU重い処理は別スレッドで
task::spawn_blocking(|| {
// 重い計算
(0..1_000_000).sum()
}).await.unwrap()
}
}
3. エラーは早めに処理
#![allow(unused)]
fn main() {
async fn fetch_and_process(url: &str) -> Result<String, Error> {
let response = reqwest::get(url).await?;
// ステータスチェック
if !response.status().is_success() {
return Err(Error::HttpStatus(response.status()));
}
let text = response.text().await?;
Ok(text)
}
}
まとめ
| 概念 | 説明 |
|---|---|
async | 非同期関数を定義 |
.await | Futureの完了を待つ |
tokio | 非同期ランタイム |
join! | 複数を並行実行 |
select! | 最初の完了を処理 |
| 注意点 | 対策 |
|---|---|
| ブロッキング | tokio::time::sleep等を使用 |
| CPU重い処理 | spawn_blockingを使用 |
| エラー処理 | ?演算子とResultを活用 |
確認テスト
Q1. async関数が返すものは?
正解: B)
async関数は呼び出しただけでは実行されません。Futureを返し、.awaitされたときに初めて実行されます。
Q2. tokio::join!(task1(), task2())の動作は?
正解: B)
join!は複数のFutureを並行に実行し、すべてが完了するまで待ちます。各タスクの待ち時間を有効活用できます。
Q3. 以下のコードの実行時間は約何秒?tokio::join!(task_a(), task_b())(task_aは2秒sleep、task_bは3秒sleep)
正解: C)
join!は両方のタスクを並行実行します。task_aは2秒、task_bは3秒かかりますが、並行なので最も長い3秒で両方完了します(順次実行なら5秒かかる)。
Q4. 非同期関数内でstd::thread::sleepを使うとどうなる?
正解: A)
std::thread::sleepはスレッド全体をブロックするため、非同期ランタイム上の他のタスクも止まってしまいます。非同期コード内ではtokio::time::sleepを使うべきです。
Q5. tokio::select!の動作として正しいのは?
正解: D)
select!は複数のFutureを並行に実行し、最初に完了したものの結果を処理します。他の未完了のFutureはキャンセルされます。タイムアウト処理などに便利です。
Phase 4 完了!
おめでとうございます!Phase 4を完了しました。
学んだこと:
- Cargoの高度な機能(ワークスペース、features)
- クレートエコシステムの活用
- テストの書き方(単体テスト、統合テスト)
- ドキュメンテーション
- 非同期プログラミング(async/await、tokio)
次のPhase: Phase 5: Webアプリ開発