S-JIS[2024-12-14/2024-12-19] 変更履歴

asyncメモ

Rust関数呼び出しのasync/awaitのメモ。


概要

async/awaitは、非同期処理を簡潔に書くための構文。

非同期処理とは、時間がかかる処理をブロックせずに実行すること。
(自分のイメージでは、時間がかかる処理を別スレッドで実行することだが、必ずしもそうではないようで、例えばI/O待ちのときに他の処理を行えるようにする事も含むらしいらしい)

Rustでは、非同期処理のためにFutureトレイトというものが用意されている。
非同期処理を行う関数は、Futureを返す。
非同期処理の結果は、Futureから取得する形となる。

このFutureを隠蔽する構文がasync/await。

ただ、Rustの標準ライブラリーではFutureトレイト(とasync/awaitの構文)を提供しているだけで、Futureの実体には別のライブラリーが提供しているものを使うらしい。
その代表がtokio。他にfuturesやasync-stdといったクレートがあるらしい。


async/await

非同期処理を行う関数(やメソッド)には、asyncを付ける。

pub(crate) async fn task1() -> i32 {
    tokio::time::sleep(Duration::from_secs(2)).await;
    123
}

asyncの付いた関数(やメソッド)を呼び出すには、awaitを付ける。
(awaitを呼び出すことで、非同期処理が開始されるらしい)

async fn call() {
    let r /*i32*/ = task1().await;
}

そして、awaitを付けて関数を呼び出す場合、呼び出す側の関数もasyncである必要がある。

そうなると、awaitの付いた関数を呼び出していると、それを呼び出す関数はasyncを付ける必要があり、
その関数を呼び出すにはawaitを付ける必要があり、それを呼び出す関数はasyncを付ける必要があり、…
を繰り返して、最終的にmain関数にもasyncを付けたくなる。
が、標準ではmain関数にasyncは付けられないので、#[tokio::main]を使うことになる。

#[tokio::main]
async fn main() {
    〜
}

同様に、テストのときはtokio::testを使う。

#[cfg(test)]
mod test{
    use tokio::test;

    #[test]
    async fn test1(){
        〜
    }
}

ブロックを使う場合もasyncを付ける。

    let future = async {
        tokio::time::sleep(Duration::from_secs(2)).await;
        456
    };

    let r /*i32*/ = future.await;

複数の非同期関数を同時に待つには、tokioのjoin!マクロを使う。

    let r /*(i32, i32)*/ = tokio::join!(task1(), task2());

async関数を普通の関数から呼ぶ方法

非同期処理(asyncが付いている関数)を同期処理(asyncの付いていない関数)から呼ぶには、block_onを使う。

ただし、同一スレッド内でネストしてblock_onを使うことは出来ない。
つまり、同一スレッド内では、block_onのブロックの中で、もう一度block_onを呼び出すことは出来ない。

そして、実行する関数に#[tokio::main]#[tokio::test]が付けられていると、実際にはblock_onが使われている。
(#[tokio::main]や#[tokio::test]はマクロであり、block_onを使ったプログラムに展開される)

したがって、自分のプログラムでは単純にblock_onを使うことは出来ない。

ちょっと無理矢理な方法ではあるが、別スレッドを起動すれば、block_onを使うことが出来る。

#[tokio::main]
async fn main() {
    std::thread::scope(|scope| {
        scope.spawn(move || {
            let runtime = tokio::runtime::Runtime::new().unwrap();
            runtime.block_on(async {
                println!("sleep start");
                tokio::time::sleep(Duration::from_secs(2)).await;
                println!("sleep end")
            });
        });
    });
}

scope.spawn()で別スレッドを起動している。

std::thread::spawn()でもスレッドを起動することが出来るが、block_onの中にselfを含めたい場合、ライフタイムの問題でコンパイルエラーになる。
scope()はブロック内の処理が終了するまで待つらしく、コンパイラーがライフタイムを認識できるからエラーにならないようだ。


asyncメソッドを含むトレイト

asyncを付けたメソッドを持つトレイトは、そのままでは構造体のフィールドに保持できない。[2024-12-19]

その場合は、traitと、そのトレイトを実装するimplに#[async_trait::async_trait]属性を付ける。
これらは同一のものを付ける必要がある。例えばasync_traitにオプションを付けているときは、それも含めて同じものにする。

Cargo.toml:

〜

[dependencies]
async-trait = "0.1.83"

src/main.rs:

#[async_trait::async_trait]
trait MyTrait {
    async fn example(&self);
}

struct MyStruct1;

#[async_trait::async_trait]
impl MyTrait for MyStruct1 {
    async fn example(&self) {
        println!("example");
    }
}

struct MyStruct2 {
    t1: Box<dyn MyTrait>,	// トレイトをフィールドに保持
}

impl MyStruct2 {
    async fn execute(&self) {
        self.t1.example().await;
    }
}

#[tokio::main]
async fn main() {
    let s2 = MyStruct2 {
        t1: Box::new(MyStruct1 {}),
    };
    s2.execute().await;
}

関数へ戻る / Rustへ戻る / 技術メモへ戻る
メールの送信先:ひしだま