ランタイムがしていることって何だっけ?

ついさっき「読んでいきましょう!」と言っておいて申し訳ないですが、もう少し、お話をしましょう。ランタイム、ランタイムと言っていますが、これは裏で何をしてくれているのでしょうか?ランタイムは普段、私達が書いているコードには登場しません。秘密裏に私達の書いたコードを並行、または並列に動作させてくれます。これによって私達は簡単に、そして安全により効率的なプログラムを書くことが出来ています。 この縁の下の力持ちが本書の主役ですが、今の所、紹介もなければ、登場すらしていません。これで主役と言えるのでしょうか?なので、これからは「ランタイムくん」にスポットライトを当てていきます。

async-std を用いたコードを少し見ていこう

2020 年 2 月現在の Rust(1.41.0)では、1 章で出てきたasync/awaitなどを使い非同期関数や非同期ブロックで非同期タスクを作る事はできます。しかし、生成した非同期タスクを実行するすべは用意されていません。そのため、非同期タスクを正しく起動して実行を監視し、きちんと非同期タスクを完了させるために非同期ランタイムを用いる必要があります。

それでは、async-std(ランタイムの一つ)を使って非同期コードを書いていくとどの様になるのでしょうか?0 ~ 9 までを画面上に出力するコードを見てみましょう。詳細な構文はここでは知る必要がないですし、今後ランタイムの中身を読んでいくときも知る必要がないように心がけます。そのため、構文の説明などはすべて省略します。

use async_std::task;

fn main() {
    let mut futures = vec![]; // ベクター初期化

    for i in 0..10 {
        // クロージャを定義
        let async_function = async move {
            println!("{}", i);
        };

        // いま定義したクロージャを実行する非同期タスクを生成 + スケジューリングしている
        let handle = task::spawn(async_function);

        // 非同期タスクのハンドラーを待機リストに入れる
        futures.push(handle);
    }

    task::block_on(async {
        for f in futures {
          // 各々の非同期タスクの完了を待つ
            f.await
        }
    });
}

このコード中に出てきたtask::spawnによって非同期タスクを生成することが出来ます。次に、task::block_onですが、これは非同期タスクの生成をしたのちその結果が返るまで現在のスレッドをブロックします。なのでこのコードはspawnで「変数 i の値を出力する」という処理をする非同期タスクを生成し、その後block_onで「各々の非同期タスク完了を待つ」という非同期タスクを生成し完了を待つという動きをします。このコードを実行すると何が出力されるでしょうか?ちょっと予想してみて下さい。

僕の環境では次のようになりました。(きっともう一度実行すると結果は変わるでしょうが。)

0
3
1
4
2
5
9
7
6
8

この結果の順序には特に意味はありませんが、0 ~ 9 までが順番に出力されないこと意味があります。async-stdのランタイムでは非同期タスクは生成した順に実行されるわけではないということが分かりますね。

また質問なのですが、このコードは並列で動作すると思いますか?それとも複数タスクを逐次実行しているのでしょうか?

1 章でも話した内容とかぶってしまいますが、このコードは「並列に動作してして欲しい」と思い記述したコードですが、実際にはプログラムのランタイムが様々な抽象化を行い「並列に動作させてくれる可能性がある」コードです。なのでこのコードだけを見て並列で動くのかは判断できません。