🦀

【rust】async/awaitのベストプラクティス

2023/10/22に公開

async/awaitとは?

rustでは、asyncを使う事で非同期処理を書くことができますが、実際にタスクを動かすにはblock_onなどを呼び出して非同期ランタイムを動かしてやる必要があります。

asyncを書くという事はFuture traitを作成するというだけで、それだけでは何も実行されず、Futureという名前の通り、将来的に実行されるものである、という事をわりと強めに認識する必要があります。

このasync/awaitとblock_onの扱いについては、調べてみても、動作原理について書いてくれている記事は多数見当たるものの、「とりあえずこうやっとけばOK」というベストプラクティス的なものを簡単に説明しているものがあまり見当たらなかったので、今回そのあたりについて書いてみようと思いました。

実際の書き方

自分のプロジェクトに使えそうなクレートを探していると、たまにasyncで書かれているライブラリが見つかると思います。
例えばHTTPでドキュメントを取得するためのクレートreqwestなんかがそうなのですが、これは関数がasyncで実装されていて、シングルスレッドのプログラムの場合だと、そのままでは使えません。

こちらのクレートのサンプルを見ると、

use std::collections::HashMap;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let resp = reqwest::get("https://httpbin.org/ip")
        .await?
        .json::<HashMap<String, String>>()
        .await?;
    println!("{:#?}", resp);
    Ok(())
}

こんな感じです。

asyncを使うプログラムのサンプルはこうなっていることが多いのですが、
main()関数がasyncになっていて、
その上の行には
#[tokio::main]
というのが書かれています。

よくわからずに、書かれている通りに同じようにしたらそれはそれで動作するのですが、
何やら得体のしれない書き方を強制され、モヤモヤとした気持ち悪さがあります。

この
#[tokio::main]
というのは、tokioというクレートを使う場合に、めんどくさい記述を代行してくれるマクロの記述です。

これは、マクロを展開すると、下記のようなコードになるようです。

fn main() {
    tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()
        .unwrap()
        .block_on(async {
            //main関数の中身
        })
}

何やらよくわからないものが出てきましたが、
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap();
こちらが、先ほどちらっと出てきた「非同期ランタイム」なるものの正体です。

.block_on()
で、Future traitを実際に実行します。

Future型

Futureというのは、処理すべきプログラムが入った箱です。
なので、

async{
	//処理
}

は変数に入れる事ができます。

let fut=async{
	//処理
};

これで、変数futにFutureが入ります。

実行するにはこうです

let fut=async{
	//処理
};
let mut rt = tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build();
rt.block_on(fut);

Futureという名前から察するに、「将来的に実行されるプログラム」とイメージしておけば概ね合っていると思われます。

rt.block_on()にFutureを渡す事で、非同期ランタイムがそれを実行する、という事です。

Futureはrust標準、tokioはサードパーティ製ライブラリ

Future traitについては、tokioを使って説明している記事だけを読んでいてもよくわからない事が多いのですが、tokioに固有の存在というわけではありません。
Futureはrustの言語自体に備わっている型です。
なのですが、rustの言語自体には、それを実行するための手段がありません。
それらの実行は、サードパーティのクレートに委ねられています。
(この辺りがFutureの理解のハードルを上げているような気がしなくもないです。)

Futureの実行によく使われるクレートがtokioというだけで、実際にはFutureを実行できるクレートであれば何を使っても同じことができますし、なんなら自力で書くこともできるようです。

例えば、futuresクレートを使ってFutureを処理することもできます。
その場合の書き方はこうです。

fn main() {
    futures::executor::block_on(async{
        //main関数の中身
    })
}

tokioを使う場合とほとんど変わらず、なんならblock_onという関数名は全く同じものになっています。

これらは、ソースコードまでは確認していませんが、おそらく微妙な差異はあるものの、結果としてはFutureが実行される、という点においては同じ結果が得られるものです。

ですので、どちらを使っても構いません。
悩むようであれば使用するクレートが推奨しているものを使えば良いですし、
なんなら混在して使用しても特に問題はないでしょう。

一つ言える事は、asyncな処理を実行するのは、必ず非同期ランタイムを使うか、自力で実装する必要があるという事です。
それ以外にFutureを実行する方法はありません。ですので、自力で非同期ランタイムを実装する、という方以外は、tokioかfuturesクレート辺りを使いましょう。

block_onは直接書いても大丈夫

#[tokio::main]
という書き方が標準っぽい顔をしていますが、場合によってはblock_onを直接記述した方が良い事もあります。
というのも、少なくとも私の場合は局所的にblock_onを使いたいケースというのがあったので、
一つのプログラムの中でblock_onを複数呼び出す事があります。

fn main() {
    futures::executor::block_on(async{
        //処理1
    });
    futures::executor::block_on(async{
        //処理2
    })
}

こんなことをしても問題はありません。

ただし、一つだけ絶対に気を付けなければならないのが、block_onの中でblock_onを書くと、ネストされたblock_onが実行された瞬間にプログラムが死ぬという事です。

そのため、
#[tokio::main]
を書くという事はmain関数全体がblock_onされるに等しく、
部分的にblock_onを書く、という事が永遠にできなくなり、内部的にblock_onを使っているライブラリを使えないという事になる場合もあります。

Rustの非同期処理は、メモリ安全性を確保するためにSendとSyncというtraitによってスレッド間でのデータ共有が可能かどうかを厳密に追跡し、使用している型によっては並列動作をさせる事ができないという事もあり、理解に苦しむ事もありますが、とりあえず
・asyncはblock_onで実行さる
・block_onの中でblock_onはできない
・Sendが実装されていない型は並列動作できない
・特定のランタイムに依存しているライブラリがあるかもしれない
といった事を覚えておけば、非同期関係の問題には対応しやすくなると思います。

Discussion