Skip to content

3.7 - Webプログラミングでasyncとawaitを探求する

Webプログラミングでasyncとawaitを探求する

非同期プログラミングについて知っていることで、次のように、Webアプリケーションのメイン関数を違った角度から見ることができるようになりました。

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .route("/", web::get().to(greet))
            .route("/{name}", web::get().to(greet))
            .route("/say/hello", web::get().to(|| async { "Hello Again!" }))
    })
    .bind("127.0.0.1:8080")?
    .workers(3)
    .run()
    .await
}

greet関数は非同期関数であり、したがってfutureであることがわかります。また、/say/helloビューに渡すクロージャもasync構文を利用していることがわかります。また、HttpServer::new関数がasync fn main()の中でawait構文を利用していることもわかります。したがって、HttpServer::new関数はエグゼキュータであると推論できます。しかし、#[actix_web::main]マクロを削除してしまうと、次のようなエラーが発生します。

`main` function is not allowed to be `async`

これは、エントリーポイントであるmain関数が、プログラムを実行するのとは対照的にfutureを返すからです。#actix_web::main]はランタイム実装で、すべてを現在のスレッドで実行できるようにします。actix_web::main]マクロは、Actixシステムによって実行される非同期関数(この場合、main関数である)をマークします。

備考

ここで雑談になりますが、Actix crateはアクターモデルに基づいて並行計算を実行します。ここでは、アクターが計算機であることを意味します。アクターは互いにメッセージを送受信することができます。アクターは自分の状態を変更することができますが、メッセージを通してのみ他のアクターに影響を与えることができるので、ロックベースの同期の必要性がなくなります(今回取り上げたミューテックスはロックベースです)。このモデルをさらに探求しても、基本的なWebアプリを開発するのには役立たないでしょう。しかし、Actixクレートには、Actixを使った並行システムのコーディングに関する良いドキュメントがあります(https://actix.rs/book/actix.

ここでは、多くのことを取り上げました。全部は覚えていないと感じても、ストレスを感じないでください。非同期プログラミングにまつわるさまざまなトピックを簡単に取り上げました。Actix Webフレームワークをベースとしたアプリケーションの構築を開始するために、それを徹底的に理解する必要はありません。

また、これまで取り上げてきた内容が過剰だったと感じるかもしれません。例えば、サーバーを立ち上げて、必要なときにasync構文を使い、何が起こっているのかよくわからないまま、単にビューをパンチアウトすることもできたでしょう。何が起こっているのかを理解せずとも、どこにasyncを置くべきかを知っていれば、おもちゃのアプリケーションを構築する際に速度を落とすことはなかったでしょう。しかし、この笛吹きツアーは、アプリケーションをデバッグしたり設計したりする際には、非常に貴重なものです。これを確立するために、野生の例を見ることができます。1つのファイルで複数のサーバーを実行する、このスマートなStack Overflowのソリューションを見ることができます: https://stackoverflow.com/questions/59642576/run-multiple-actix-app-on-different-ports.

Stack Overflowのソリューションのコードは、基本的に1つのランタイムで2つのサーバーを実行することを含んでいます。まず、彼らは以下のコードでビューを定義します。

use actix_web::{web, App, HttpServer, Responder};
use futures::future;
async fn utils_one() -> impl Responder {
    "Utils one reached\n"
}
async fn health() -> impl Responder {
    "All good\n"
}

ビューが定義されると、2つのサーバーがメイン関数で定義されます。

#[actix_rt::main]
async fn main() -> std::io::Result<()> {
    let s1 = HttpServer::new(move || {
            App::new()
                .service(web::scope("/utils")
                .route("/one", web::get().to(utils_one)))
        })
        .bind("0.0.0.0:3006")?
        .run();
    let s2 = HttpServer::new(move || {
            App::new()
                .service(web::resource("/health")
                .route(web::get().to(health)))
        })
        .bind("0.0.0.0:8080")?
        .run();
    future::try_join(s1, s2).await?;
    Ok(())
}

このコードには何の表記も加えていないが、あなたを脅かすことはないだろう。s1とs2がrun関数が返すfutureであることは確信を持って推論できる。そして、この2つのフューチャーを結合して、終了を待ちます。また、私たちのコードとStack Overflowのソリューションのコードには、若干の違いがあります。私たちのソリューションでは、await? を利用し、次のコードスニペットでOkを返します。

    future::try_join(s1, s2).await?;
    Ok(())
}

これは、?演算子が本質的にトライマッチだからです。 join(s1, s2).await? は、おおよそ次のようなコードに展開されます。

match join(s1, s2).await {
    Ok(v) => v,
    Err(e) => return Err(e.into()),
}

演算子「?」を実装することで、メイン関数がResultを返すため、ソリューションを提供する側は最後にOkを挿入する必要がありましたが、これが解消されました。

このように、野生のソリューションにおいて、Stack Overflowは非同期プログラミングをカバーすることの重要性を実証しています。私たちは、野生のコードを見て、何が起こっているのか、Stack Overflowの投稿者がどのように達成できたのかを解明することができます。これは、私たち自身がクリエイティブになれるということでもあります。3つのサーバを作り、それをメイン関数で実行することを止めることはできません。これこそRustの真骨頂です。Rustの学習に時間をかけることで、低レベルの領域に安全に飛び込むことができ、自分の行動をより細かくコントロールすることができます。これは、Rustを使ったプログラミングのどの分野でも言えることでしょう。

アプリケーションを構築する前に、もう1つ調査すべき概念があります。それはランタイムです。メイン関数をfutureにするために、Actix Webマクロが必要なことは分かっています。Tokioクレートを見てみると、ネットワークアプリケーションを書くのに必要なビルディングブロックを提供することで、Rustプログラミング言語用の非同期ランタイムであることがわかります。Tokioの仕組みは複雑です。しかし、ランタイムの高速化に関するTokioのドキュメントを見ると、次のような図を追加することができます。

図 3.3 - トキオのランタイムを高速化する [source:トキオ ドキュメント(2019) (https://tokio.rs/blog/2019-10-scheduler)] を参照。

先の図では、キューに入れられたタスクとそれを処理するプロセッサーがあることがわかります。先ほどのタスクの処理で、見覚えがあるはずです。このように考えると、Actix Webマクロの代わりにTokioを使ってサーバを動かすことができることは、それほど衝撃的なことではないかもしれません。そのためには、Cargo.tomlファイルに次のコードでTokioの依存関係を定義します。

tokio = { version = "1.17.0", features = ["full"] }

先のコードで、main.rsファイル内のマクロを以下のコードで切り替えることができるようになりました。

#[tokio::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .route("/", web::get().to(greet))
            .route("/{name}", web::get().to(greet))
    })
    .bind("127.0.0.1:8080")?
    .bind("127.0.0.1:8081")?
    .workers(3)
    .run()
    .await
}

このコードを実行すると、サーバーを実行したのと同じ結果が得られます。Actixのランタイムマクロの代わりにTokioを使用した場合、いくつかの矛盾が生じる可能性があります。これは、自信を持ってサーバーを構成できることを示す興味深い結果ですが、ActixでToDoアプリケーションを開発する場合、この本の残りの部分ではActixランタイムマクロを使用することにします。Tokioについては、第14章「Tokioフレームワークの探索」で再確認する予定です。

ここまでで、サーバーの設定と、サーバーがどのようにリクエストを処理するかについて、十分に説明しました。次のセクションでは、ビューの定義とその処理方法について説明します。