Rust Webアプリケーションにおける状態共有
James Reed
Infrastructure Engineer · Leapcell

堅牢でスケーラブルなWeb Serviceの構築は、複数のスレッドや非同期タスク間で共有リソースを管理することがしばしば必要となります。Rustの世界では、その厳格な所有権ルールとスレッド安全性への注力により、この課題は特に興味深く、意図的な形で現れます。Arc<Mutex<T>> パターンはRustにおける並行プログラミングの基本的な礎石ですが、actix-webのようなWebフレームワークは独自の便利な抽象化を導入しています。この記事では、マルチスレッドWebアプリケーションのコンテキストにおける状態共有のニュアンスを探り、特に Arc<Mutex<T>> の直接的な使用と actix-web の web::Data<T> を比較し、実践的な例でその適用方法を説明します。これらのパターンを理解することは、Rustで高性能なスレッドセーフなWeb Serviceを構築する上で不可欠です。
並行状態管理のためのコアコンセプト
比較に入る前に、Rustにおける並行状態管理の基盤となるコアコンポーネントを簡単に定義しましょう。
-
std::sync::Arc<T>(Atomic Reference Counted): このスマートポインタは、値の共有所有権を提供します。複数のArcインスタンスが同じデータを示すことができ、データはそれを指す最後のArcがドロップされたときにのみドロップされます。重要なことに、Arcは共有データをスレッド間で安全に渡すことを可能にします。これはしばしば内部可変性型と組み合わされます。 -
std::sync::Mutex<T>(Mutual Exclusion): 共有データへの同時アクセスから保護するためのプリミティブです。Mutex内のデータにアクセスするために、スレッドはまずロックを取得する必要があります。ロックがすでに別スレッドによって保持されている場合、現在のスレッドはロックが解放されるまでブロックされます。これにより、一度に1つのスレッドのみがデータを変更できるようになり、競合状態を防ぎます。 -
actix_web::web::Data<T>: これはArc<T>のactix-webによる便利なラッパーです。これにより、共有アプリケーション状態をactix-webアプリケーションに登録し、ハンドラがエクストラクタとして自動的に利用できるようにします。本質的に、Data<T>はWebアプリケーションに合わせて調整された人間工学的なシュガーを追加したArc<T>です。 -
ハンドラとミドルウェア:
actix-webでは、ハンドラはHTTPリクエストに応答する関数であり、ミドルウェア関数はハンドラの前または後にリクエストとレスポンスを処理できます。どちらも共有アプリケーション状態へのアクセスを必要とすることがよくあります。
リソースの共有: Arc<Mutex<T>> vs. actix_web::web::Data<T>
根本的に、actix_web::web::Data<T> と手動で管理される Arc<Mutex<T>> は同じ基本的な目的を果たします: actix-web アプリケーション内で共有され、スレッドセーフな状態へのアクセスを提供します。主な違いは、その統合と利便性にあります。
直接的な Arc<Mutex<T>> アプローチ
Arc<Mutex<T>> を直接管理する場合、共有データ構造を明示的にラップし、Arc のクローンを必要な場所に渡します。これは最大限の柔軟性を提供しますが、特にサーバーのセットアップ時には、わずかに冗長になる可能性があります。
複数のリクエストがインクリメントまたはデクリメントできるシンプルなカウンタを考えてみましょう。
use std::sync::{Arc, Mutex}; use actix_web::{web, App, HttpServer, Responder, HttpResponse}; // 共有アプリケーション状態 struct AppState { counter: Mutex<i32>, } async fn increment_counter(data: web::Data<Arc<AppState>>) -> impl Responder { let mut counter = data.counter.lock().unwrap(); *counter += 1; HttpResponse::Ok().body(format!("Counter: {}", *counter)) } async fn get_counter(data: web::Data<Arc<AppState>>) -> impl Responder { let counter = data.counter.lock().unwrap(); HttpResponse::Ok().body(format!("Counter: {}", *counter)) } #[actix_web::main] async fn main() -> std::io::Result<()> { let app_state = Arc::new(AppState { counter: Mutex::new(0), }); HttpServer::new(move || { App::new() .app_data(web::Data::new(Arc::clone(&app_state))) // Arc<AppState> を登録 .route("/increment", web::post().to(increment_counter)) .route("/get", web::get().to(get_counter)) }) .bind(("127.0.0.1", 8080))? // binds to localhost:8080 .run() .await }
この例では、AppState は Mutex<i32> を含んでいます。Arc<AppState> を作成し、app_data を呼び出すときに明示的にクローンします。ハンドラは web::Data<Arc<AppState>> をエクストラクタとして期待します。これは Arc<Mutex<T>> の生の力を示しており、完全に機能します。
actix_web::web::Data<T> アプローチ
actix_web::web::Data<T> は、Arc<T> (あるいは直接 Arc<Mutex<T>> でさえ)をラップし、それを直接エクストラクタとして提供することでパターンを簡略化します。web::Data::new(my_state) を使用すると、actix-web は内部的に Arc の作成とクローンを処理し、セットアップをよりクリーンにします。
前の例を web::Data<T> をより慣用的な方法でリファクタリングしてみましょう。
use std::sync::Mutex; use actix_web::{web, App, HttpServer, Responder, HttpResponse}; // 共有アプリケーション状態 - ここで明示的なArcは不要、Dataが処理します struct AppState { counter: Mutex<i32>, } async fn increment_counter_data(data: web::Data<AppState>) -> impl Responder { let mut counter = data.counter.lock().unwrap(); *counter += 1; HttpResponse::Ok().body(format!("Counter: {}", *counter)) } async fn get_counter_data(data: web::Data<AppState>) -> impl Responder { let counter = data.counter.lock().unwrap(); HttpResponse::Ok().body(format!("Counter: {}", *counter)) } #[actix_web::main] async fn main() -> std::io::Result<()> { // 状態を直接 web::Data に渡すことができます。内部でArcでラップされます let app_state = web::Data::new(AppState { counter: Mutex::new(0), }); HttpServer::new(move || { App::new() .app_data(app_state.clone()) // Data<AppState> をクローン .route("/increment", web::post().to(increment_counter_data)) .route("/get", web::get().to(get_counter_data)) }) .bind(("127.0.0.1", 8080))? // binds to localhost:8080 .run() .await }
注目すべき主な違いは次のとおりです。
mainでは、AppStateはweb::Data::new()によって直接ラップされ、内部的にArcが暗黙的に使用されます。app_data(app_state.clone())の行は、生のArcではなく、web::Dataインスタンス自体をクローンします。- ハンドラは単に
web::Data<AppState>を受け取るだけで、アクセスが簡単になります。
どちらを選択するか
-
一般的なアプリケーション状態には
web::Data<T>を使用: これは、actix-webアプリケーション内のすべてのハンドラからアクセスする必要がある設定、データベース接続プール、またはその他のグローバル状態を共有するための推奨される、最も人間工学的な方法です。Arcのボイラープレートを抽象化します。 -
カスタムオブジェクト階層や複雑なシナリオでの直接
Arc<Mutex<T>>: ハンドラに直接的ではないが、同じ状態を共有し、Rustの並行モデルに準拠する必要がある内部コンポーネントやサービスがある場合、それらの構造内で明示的にArc<Mutex<T>>を管理することで、より多くの制御が得られます。たとえば、同じ状態も変更するバックグラウンドワーカー スレッドを構築する場合、直接Arc<Mutex<T>>のクローンを渡すことになります。web::Data<T>は内部的にはArc<T>ですが、主にactix-webハンドラのための エクストラクタ として設計されています。
アプリケーションシナリオ
両方のパターンは、一般的なWeb Serviceタスクに不可欠です。
- データベース接続プール:
Arc<PgPool>またはArc<SqlitePool>は、すべてのハンドラにデータベースアクセスを提供するためにweb::Dataでラップされることがよくあります。 - 設定設定: グローバルアプリケーション設定は一度ロードされ、
web::Data<AppConfig>経由で利用可能になります。 - キャッシング: リクエスト間で共有されるインメモリキャッシュは、通常、
Arc<Mutex<HashMap<K, V>>>または同様のものをweb::Dataを介して公開します。 - レート制限: ユーザー/IPアドレスごとのリクエスト数を追跡するグローバルレート制限状態は、間違いなく
Arc<Mutex<T>>を含みます。
結論
マルチスレッド Rust Webアプリケーションでリソースを共有するには、スレッド安全性と所有権を慎重に検討する必要があります。Arc<Mutex<T>> パターンは、これを実現するための基本的な要素を提供し、変更可能なデータへの安全な同時アクセスを保証します。actix-web の web::Data<T> は、この基盤の上に構築されており、アプリケーション固有の状態をハンドラに注入するための人間工学的で慣用的な方法を提供します。どちらのパターンも最終的には Arc を活用して同様の結果を達成しますが、web::Data<T> は一般的なWebアプリケーションの状態管理の開発者エクスペリエンスを簡素化し、actix-web サービスに共有リソースをシームレスに統合するための好ましい選択肢となります。

