【c#】async/await 理解度チェック【非同期処理】


c#で非同期処理を円滑に行うことができる async/await であるが、どのような挙動になるかをきちんと理解できていますか?
正しく理解できているかをチェックできる問題を用意したので、ぜひチャレンジしてみよう。

問題

以下のコードを実行したときに標準出力に出力される内容を記述せよ。
ただし、Thread.Sleep(int milliSecondsTimeout)Task.Delay(int milliSecondsDelay)メソッド以外の処理にかかる時間は 0 とする。

例題 1

class Program
{
    static void Main(string[] args)
    {
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        Thread.Sleep(1000);
        Console.WriteLine($"経過時間:{stopwatch.ElapsedMilliseconds}");
    }
}

回答

経過時間:1000

解説

この例題は問題設定を理解するためのもので、async/await は関係ない。
Stopwatch クラスのインスタンスを作成、計測開始し、1000 ミリ秒停止後に経過時間(ミリ秒)を標準出力へ出力するというプログラムである。
問題の設定上、Thread.Sleep にかかる時間以外の処理時間は考慮しないため、出力される内容は 経過時間:1000 となる。
なお実際にこのプログラムを動かしてみると、Thread.Sleep で停止している以外にもわずかに処理時間が発生するため、ぴったり 1000 にはならない場合もある。
また、実際にコンソールアプリで実行してみると、プログラムが終了するとすぐにコンソール画面が閉じてしまうので、Console.Readline();などで入力待ちの状態にしておくと出力内容が確認しやすいと思う。
さて、問題の設定が理解できたところで、次からはいよいよ実際の問題に挑戦してみよう。

問題 1

    static async Task Main(string[] args)
    {
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        var returnValue = await AsyncMethod(1000,"A",stopwatch);
        Console.WriteLine(returnValue);
    }
    public static async Task<string> AsyncMethod(int delay,string id, Stopwatch stopwatch)
    {
        Console.WriteLine($"{id} 開始 経過時間:{stopwatch.ElapsedMilliseconds}");
        await Task.Delay(delay);
        Console.WriteLine($"{id} 終了 経過時間:{stopwatch.ElapsedMilliseconds}");
        return $"{id} 戻り値";
    }

回答

A 開始 経過時間:0
A 終了 経過時間:1000
A 戻り値

解説

AsyncMethodという非同期メソッドを非同期に実行するプログラムである。
awaitを使っているので、メインスレッドは非同期メソッドの処理が完了するまで待機している。
なので、最初にAsyncMethod内の処理がすぐに実行され、「A 開始 経過時間:0」が出力される。
そのあと、1000 ミリ秒待機し、「A 終了 経過時間:1000」が出力される。
そして「A 戻り値」という戻り値を返して AsyncMethod の処理が終了し、メインスレッドの待機が終了、戻り値をreturnValueへ格納する。
その後、標準出力に戻り値を出力する。
回答自体は難しくないと思うが、await を使用したことにより非同期メソッドが別のスレッドで行われるようになったという点に注意したい。

問題 3

class Program
{
    static async Task Main(string[] args)
    {
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        await AsyncMethod(1000, "A", stopwatch);
        await AsyncMethod(1500, "B", stopwatch);
    }
    public static async Task<string> AsyncMethod(int delay, string id, Stopwatch stopwatch)
    {
        Console.WriteLine($"{id} 開始 経過時間:{stopwatch.ElapsedMilliseconds}");
        await Task.Delay(delay);
        Console.WriteLine($"{id} 終了 経過時間:{stopwatch.ElapsedMilliseconds}");
        return $"{id} 戻り値";
    }
}

回答

A 開始 経過時間:0
A 終了 経過時間:1000
B 開始 経過時間:1000
B 終了 経過時間:2500

解説

await AsyncMethod(1000, "A", stopwatch);
await AsyncMethod(1500, "B", stopwatch);

がポイント。どちらも await しているので、A が終了を待機してから B の処理を開始するため、回答のような結果となる。
B 開始時の経過時間が 1000 となっているのが重要な点である。

問題 4

    class Program
    {
        static async Task Main(string[] args)
        {
            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();
            var task1 = AsyncMethod(1000, "A", stopwatch);
            var task2 = AsyncMethod(1500, "B", stopwatch);
            Console.WriteLine($"{task1.Result} 経過時間:{stopwatch.ElapsedMilliseconds}");
            Console.WriteLine($"{task2.Result} 経過時間:{stopwatch.ElapsedMilliseconds}");
        }
        public static async Task<string> AsyncMethod(int delay, string id, Stopwatch stopwatch)
        {
            Console.WriteLine($"{id} 開始 経過時間:{stopwatch.ElapsedMilliseconds}");
            await Task.Delay(delay);
            Console.WriteLine($"{id} 終了 経過時間:{stopwatch.ElapsedMilliseconds}");
            return $"{id} 戻り値";
        }
    }

回答

A 開始 経過時間:0
B 開始 経過時間:0
A 終了 経過時間:1000
A 戻り値 経過時間:1000
B 終了 経過時間:1500
B 戻り値 経過時間:1500
(※1 行目、2 行目は順不同)

解説

var task1 = AsyncMethod(1000, "A", stopwatch);
var task2 = AsyncMethod(1500, "B", stopwatch);

await がないため戻り値はTask<T>となり、Task の終了は待機せず、メインスレッドは次の処理に進む。
そして、Task は作成したときから処理が並列で行われる。
そのため、

A 開始 経過時間:0
B 開始 経過時間:0

となる。なお、問題の設定上、Task.Delay にかかる時間以外は 0 としているため、実際に実行してみれば A が先になる場合が多いだろうが、並列で行われるため、この2行は上下逆でもよい。
次に

Console.WriteLine($"{task1.Result} 経過時間:{stopwatch.ElapsedMilliseconds}");

であるが、ここでtask1.Resultが使用されているため、task1 の結果が返るまで待機する。

A 終了 経過時間:1000
A 戻り値 経過時間:1000

そして、

Console.WriteLine($"{task2.Result} 経過時間:{stopwatch.ElapsedMilliseconds}");

についても同様に、ここで task2 の結果が返るまで待機する。

B 終了 経過時間:1500
B 戻り値 経過時間:1500

なお、ここでtask1.Resultを使用しているが、この使い方だと、待機が同期的に行われ、メインスレッドが固まってしまうため、あまり良い書き方ではないということを書き添えておきたい。より良い書き方については問題6を参考にしてください。

問題 5

    class Program
    {
        static async Task Main(string[] args)
        {
            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();
            var task1 = AsyncMethod(1000, "A", stopwatch);
            var task2 = AsyncMethod(1500, "B", stopwatch);
            Console.WriteLine($"{task2.Result} 経過時間:{stopwatch.ElapsedMilliseconds}");
            Console.WriteLine($"{task1.Result} 経過時間:{stopwatch.ElapsedMilliseconds}");
        }
        public static async Task<string> AsyncMethod(int delay, string id, Stopwatch stopwatch)
        {
            Console.WriteLine($"{id} 開始 経過時間:{stopwatch.ElapsedMilliseconds}");
            await Task.Delay(delay);
            Console.WriteLine($"{id} 終了 経過時間:{stopwatch.ElapsedMilliseconds}");
            return $"{id} 戻り値";
        }
    }

回答

A 開始 経過時間:0
B 開始 経過時間:0
A 終了 経過時間:1000
B 終了 経過時間:1500
B 戻り値 経過時間:1500
A 戻り値 経過時間:1500
(※1 行目、2 行目は順不同)

解説

Console.WriteLine($"{task2.Result} 経過時間:{stopwatch.ElapsedMilliseconds}");
Console.WriteLine($"{task1.Result} 経過時間:{stopwatch.ElapsedMilliseconds}");

ポイントはココ。問題 4 から task2 と task1 が入れ替わっている。
ここでtask2.Resultが使用されているため、task2 の結果が返るまで待機するが、それまでに A は終了するため、

A 終了 経過時間:1000
B 終了 経過時間:1500
B 戻り値 経過時間:1500

という順番で出力される。
そして、次の

Console.WriteLine($"{task1.Result} 経過時間:{stopwatch.ElapsedMilliseconds}");

については、task2 の完了を待機している間に task1 は完了しているため、すぐさま
A 戻り値 経過時間:1500
が出力される。

問題 6

class Program
{
    static async Task Main(string[] args)
    {
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        var task1 = AsyncMethod(1000, "A", stopwatch);
        var task2 = AsyncMethod(1500, "B", stopwatch);
        await Task.WhenAll(task1, task2);
        Console.WriteLine($"{task1.Result} 経過時間:{stopwatch.ElapsedMilliseconds}");
        Console.WriteLine($"{task2.Result} 経過時間:{stopwatch.ElapsedMilliseconds}");
    }
    public static async Task<string> AsyncMethod(int delay, string id, Stopwatch stopwatch)
    {
        Console.WriteLine($"{id} 開始 経過時間:{stopwatch.ElapsedMilliseconds}");
        await Task.Delay(delay);
        Console.WriteLine($"{id} 終了 経過時間:{stopwatch.ElapsedMilliseconds}");
        return $"{id} 戻り値";
    }
}

回答

A 開始 経過時間:0
B 開始 経過時間:0
A 終了 経過時間:1000
B 終了 経過時間:1500
A 戻り値 経過時間:1500
B 戻り値 経過時間:1500
(※1 行目、2 行目は順不同)

解説

await Task.WhenAll(task1, task2);

問題 4 から上記が追加となった。ここで task1、task2 が両方とも完了となるまで非同期に待機する。
そのあと、

Console.WriteLine($"{task1.Result} 経過時間:{stopwatch.ElapsedMilliseconds}");
Console.WriteLine($"{task2.Result} 経過時間:{stopwatch.ElapsedMilliseconds}");

が処理され、こちらは待機後はそれぞれの task はどちらも完了となっているため、すぐに行われる。

通常、複数のTaskを待機する場合は非同期で待機するこちらの書き方をお勧めしたい。

まとめ

今回は async/await を使用した Task の基本的なところを理解できているかどうか確認できるような問題にしてみた。
これを通して理解を深めてもらえたら嬉しいです。


Programming Blog