🍢

【Rust】thread::spawnとFutures(async/await)の使い分け

2024/02/06に公開

はじめに

Rustで並列処理を行う場合、大別するとthread::spawnなどのOSスレッドか、Futuresを用いた非同期ランタイムの二つが選択肢となります。

ですので、どっちを使ったら良いのかよくわかんね。となりがちです。

対象読者

・Futures(async/await)について何となくの知識がある方
・またはFutures(async/await)については理解しているけどthread::spawnはあまり使わない、という方

async/awaitについて過去に書いた記事が下記にありますので、合わせて読んでいただければと思います。
https://zenn.dev/woden/articles/9a6c3b26b89e0a

結論

結論から言うと、基本はFuturesを使うのが良いと思います。
理由としては、asyncというものが言語の基本仕様として組み込まれており、async前提で組まれたライブラリも多いためです。
基本仕様なだけあって、取り扱いについても比較的容易です。

ただし、場合によってはthread::spawnを使った方が良いケースもあります。
今回は、主にその辺りについて考えていきたいと思います。

thread::spawnとFutureの違い

thread::spawnとの違いですが、

Future(async/await)は、何らかの処理の結果を取得し、別の何らかの処理でその結果を使用する、という事を前提にした設計になっているようです。
そのため、必ずどこかでawaitを呼び出し、処理の終了を待つことになります。(ついでにいうとawaitを呼び出すまで処理が開始される事もありません。)

async fn hoge()->u32{
	//何らかの処理
}

let a=hoge().await; //ここでhoge()の終了を待つ

let b=1; //↑の処理が実行されるまで待つ

それに対し、thread::spawnを使用した場合、新しいスレッドが起動し、即座に処理が開始されます。
また、処理終了を待つことも出来ますが、ほったらかしにすることもできます。

fn hoge()->u32{
	//何らかの処理
}
let handle=thread::spawn(||hoge()); //すぐに実行され、次の処理へ
handle.join().unwrap(); //処理完了を待つこともできる

let b=1; //↑の処理が実行されるまで待つ。

↓処理終了を待たないケース

fn hoge()->u32{
	//何らかの処理
}
thread::spawn(||hoge()); //すぐに実行され、次の処理へ

let b=1; //spawnの実行結果を待つことなく実行される。

join()を呼び出さなかった場合、メインスレッドの裏側で処理が勝手に進行します。
別スレッドでの処理の結果を受けて何か処理を行うような必要がなければこれでおけまるです。
時間のかかる処理をバックグラウンドで処理したい場合、もし適用可能であれば、このパターンが最適だと思います。

async fnとblock_on

async fnはasync fnかblock_onの中でしか使えない。という制約があります。

例えば、

async fn hoge1(){
}

fn hoge2(){
    hoge1().await;
}

のようなコードを書いてもコンパイラに叱られます。

だからといって、asyncではないfnの中でblock_onを呼び出すことは避けた方が良いでしょう。

async fn hoge1(){
}

fn hoge2(){
    futures::executor::block_on(
        hoge1()
    );
}

このコードはコンパイルも通りますし、普通に動作する場合もあります。

しかし、

async fn hoge1(){
}

fn hoge2(){
    futures::executor::block_on(
        hoge1()
    );
}

async fn hoge3(){
    hoge2();
}

fn hoge4(){
    futures::executor::block_on(
        hoge3()
    );
}
hoge4();

のような事をすると死にます。

これは、block_onの中でblock_onを呼び出せない。という制約があるためです。

そのため、hoge2のような事をやってしまうと、hoge3のようなasync fnから呼び出せない、という状況に陥ります。

そのため、asyncではないfnでblock_onを呼び出すという事はやってはいけないのです。特にライブラリとして公開するような関数でこれをやると使い勝手が悪くなるかもしれません。

なぜblock_onの中でblock_onを呼び出せないのか?

block_onが実行されると、block_on内で新たに生えてきたasync fnはblock_onを実行中のスレッドの支配下に置かれます。そのような状況で勝手に新しい支配者を誕生させようとしたところで、管理の主体は一つしかないスレッドですので、新たに生えてきたasync fnは誰に従えばいいのかわからなくなるため、そういう事はできませんよ、という事だと思います。

つまり、block_onは新しいスレッドを産むのではなく、単にそのスレッドでFuturesの管理を開始しますよ、という事に過ぎないのです。
知らんけど。

そこでthread::spawnの出番です

thread::spawnはblock_onと違い、新しいスレッドを起動します。
block_onは新しいスレッドを立てるわけではなく、thread::spawnは新しいスレッドを立てます。

つまり、
thread::spawnの中でblock_onをする事で、spawnを呼び出したスレッドとは別の独立したスレッド内でasync fnが使えるので、block_onの中でblock_onした事にはならない
という事です。

先ほどの例を書き換えてみます。

async fn hoge1() {}

fn hoge2() {
    std::thread::spawn(|| {
        futures::executor::block_on(hoge1());
    });
}

async fn hoge3() {
    hoge2();
}

fn hoge4() {
    futures::executor::block_on(hoge3());
}
hoge4();

これなら、hoge2の中のstd::thread::spawnによって独立したスレッドが建ち、その中で新しい支配者が生まれているだけなので、お互いに干渉せずasync fnを生やす事ができるのです。

thread::spawnの罠

しかし、上記のような単純な例の場合だと問題はないのですが、実際はオブジェクトのメソッド、つまり&selfを受け取る関数内でtherad::spawnを呼び出し、spawn内でselfを使いたい場合が多いと思います。

struct Hoge{};

impl Hoge{
    async fn hoge1(&self) {}
    
    fn hoge2(&self) {
        std::thread::spawn(|| {
            futures::executor::block_on(self.hoge1());
        });
    }
    
    async fn hoge3(&self) {
        self.hoge2();
    }
    
    fn hoge4(&self) {
        futures::executor::block_on(self.hoge3());
    }
}
let hoge=Hoge{};
hoge.hoge4();

この場合、hoge2はコンパイルが通りません。

std::thread::spawnで生み出されたスレッドがselfより長生きするかもしれないので、Borrrow checkerに叱られます。

しかし、これを回避する方法があります。std::thread::scopeを使う方法です。

std::thread::scope(|s| {
    s.spawn(|| {
        futures::executor::block_on(self.hoge1());
    });
});

これを使うとscopeを抜けるときに強制的にスレッドがjoinされるらしく、結果としてselfよりthreadの方が早く寿命を迎える事が保証され、コンパイルが通ります。

ただし、scopeを抜ける際に処理終了を待つので、時間が長くかかる処理を完全にバックグラウンドで動作させたい、というようなケースには不向きだと思います。

そのような場合はArcなどをうまく使って、長寿な値をスレッドに渡すようにしましょう。

まとめ

纏めると、thread::spawnを使用したいケースとしては、

・block_onの中でblock_onしたい時
・時間のかかる処理を、結果を待たずにバックグラウンドで実行したい時

といった辺りが該当すると思います。

Discussion