非同期コードの実行
HTTPサーバーは複数のクライアントを並行して処理できるべきです。 つまり、現在のリクエストを処理する前に、以前のリクエストが完了するのを待つべきではありません。 本書では、 この問題を解決しています 各接続をそれぞれ専用のスレッドで処理するスレッドプールを作成することによってです。 ここでは、スレッドを追加してスループットを向上させる代わりに、非同期コードを使って同じ効果を実現します。
handle_connection を async fn として宣言し、future を返すように変更しましょう。
async fn handle_connection(mut stream: TcpStream) {
//<-- snip -->
}
関数宣言に async を追加すると、その戻り値の型は
ユニット型 () から Future<Output=()> を実装する型に変わります。
これをコンパイルしようとすると、コンパイラはこれが動作しないことを警告します。
$ cargo check
Checking async-rust v0.1.0 (file:///projects/async-rust)
warning: unused implementer of `std::future::Future` that must be used
--> src/main.rs:12:9
|
12 | handle_connection(stream);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_must_use)]` on by default
= note: futures do nothing unless you `.await` or poll them
handle_connection の結果を await も poll もしていないため、
それは決して実行されません。サーバーを実行してブラウザで 127.0.0.1:7878 にアクセスすると、
接続が拒否されることがわかります。私たちのサーバーはリクエストを処理していません。
同期コードの中だけでは、future を await したり poll したりすることはできません。
future のスケジューリングと完了までの実行を処理するために、非同期ランタイムが必要になります。
非同期ランタイム、エグゼキューター、リアクターの詳細については、
ランタイムの選択に関するセクション
を参照してください。
一覧にあるどのランタイムでもこのプロジェクトでは動作しますが、これらの例では
async-std クレートを使用することにしました。
非同期ランタイムの追加
次の例では、同期コードを非同期ランタイムを使用するようにリファクタリングする方法を示します。ここでは async-std を使います。
async-std の #[async_std::main] 属性を使うと、非同期の main 関数を書けます。
これを使用するには、Cargo.toml で async-std の attributes 機能を有効にします。
[dependencies.async-std]
version = "1.6"
features = ["attributes"]
最初のステップとして、非同期の main 関数に切り替え、
非同期版の handle_connection が返す future を await します。
次に、サーバーがどのように応答するかをテストします。
これは次のようになります。
#[async_std::main]
async fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
// Warning: This is not concurrent!
handle_connection(stream).await;
}
}
では、私たちのサーバーが接続を並行して処理できるかをテストしてみましょう。
handle_connection を非同期にしただけでは、サーバーが
複数の接続を同時に処理できることを意味しません。その理由はすぐにわかります。
これを説明するために、遅いリクエストをシミュレートしてみましょう。
クライアントが 127.0.0.1:7878/sleep にリクエストを行うと、
サーバーは5秒間スリープします。
use std::time::Duration;
use async_std::task;
async fn handle_connection(mut stream: TcpStream) {
let mut buffer = [0; 1024];
stream.read(&mut buffer).unwrap();
let get = b"GET / HTTP/1.1\r\n";
let sleep = b"GET /sleep HTTP/1.1\r\n";
let (status_line, filename) = if buffer.starts_with(get) {
("HTTP/1.1 200 OK\r\n\r\n", "hello.html")
} else if buffer.starts_with(sleep) {
task::sleep(Duration::from_secs(5)).await;
("HTTP/1.1 200 OK\r\n\r\n", "hello.html")
} else {
("HTTP/1.1 404 NOT FOUND\r\n\r\n", "404.html")
};
let contents = fs::read_to_string(filename).unwrap();
let response = format!("{status_line}{contents}");
stream.write(response.as_bytes()).unwrap();
stream.flush().unwrap();
}
これは、The Book の
遅いリクエストのシミュレーション
と非常によく似ていますが、重要な違いが1つあります。
ブロッキング関数 std::thread::sleep の代わりに、ノンブロッキング関数 async_std::task::sleep を使用しています。
コードの一部が async fn 内で実行され、await されているとしても、それが依然としてブロックする可能性があることを覚えておくことが重要です。
私たちのサーバーが接続を並行して処理するかをテストするには、handle_connection がノンブロッキングであることを確認する必要があります。
サーバーを実行すると、127.0.0.1:7878/sleep へのリクエストが
他のすべての受信リクエストを5秒間ブロックすることがわかります!
これは、handle_connection の結果を await している間に
進行できる他の並行タスクが存在しないためです。
次のセクションでは、非同期コードを使って接続を並行して処理する方法を見ていきます。