🎼

C# の async/await の挙動で誤解していたこと

2021/07/30に公開

C# の非同期処理を勉強している中での勉強メモです。

恐らく、普通に async/await を理解している人にとっては当たり前の内容だと思いますが、触り始めの方の参考になればと思い記事にしてみました🙂

コードは こちらの記事 を参考に、ちょこっと変えています。

using System;
using System.Threading;
using System.Threading.Tasks;

namespace AsyncExample01
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
            CallTaskAsync();
        }

        private static async void CallTaskAsync()
        {
            Console.WriteLine("CallTaskAsync: START");
            Task<int> task = Task.Run<int>(new Func<int>(Calculate));
            Console.WriteLine("CallTaskAsync: Before");
            int result = await task;
            Console.WriteLine("CallTaskAsync: After");
            Console.WriteLine("CallTaskAsync: END");
        }

        private static int Calculate()
        {
            Console.WriteLine("Calculate: START");
            int total = 0;
            for (int i = 0; i <= 100; ++i)
                total += i;
            Thread.Sleep(1000);
            Console.WriteLine("Calculate: END");
            return total;
        }
    }
}

上記のコードで、こんな風に考えてしまっていました。

「ふむふむ。await があると非同期処理になって処理が進むから、下のような出力になるのかな?」

Hello World!
CallTaskAsync: START
CallTaskAsync: Before
Calculate: START
CallTaskAsync: After
CallTaskAsync: END
Calculate: END

しかし。得られた出力は下記の通り。

Hello World!
CallTaskAsync: START
CallTaskAsync: Before
Calculate: START

ここでプログラムは終了します。

出力から考えると、int result = await task; のところに来た後、プログラムごと終わっている感じです。

予想される挙動としては、int result = await task; の後は CallTaskAsync() の次の行が実行されるのではなく CallTaskAsync() が一旦リターンして Main() に戻り、そのまま Main() が終了したのでプログラムも終了した、という感じでしょうか。

そこで、コードを下記のように書き換えて挙動を確認してみました。

using System;
using System.Threading;
using System.Threading.Tasks;

namespace AsyncExample01
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
            CallTaskAsync();
            Console.WriteLine("Waiting...");
            Thread.Sleep(2000);
        }

        private static async void CallTaskAsync()
        {
            Console.WriteLine("CallTaskAsync: START");
            Task<int> task = Task.Run<int>(new Func<int>(Calculate));
            Console.WriteLine("CallTaskAsync: Before");
            int result = await task;
            Console.WriteLine("CallTaskAsync: After");
            Console.WriteLine("CallTaskAsync: END");
        }

        private static int Calculate()
        {
            Console.WriteLine("Calculate: START");
            int total = 0;
            for (int i = 0; i <= 100; ++i)
                total += i;
            Thread.Sleep(1000);
            Console.WriteLine("Calculate: END");
            return total;
        }
    }
}

すると、出力は下記のようになりました。これは想定通りの出力です。

Hello World!
CallTaskAsync: START
CallTaskAsync: Before
Calculate: START
Waiting...
Calculate: END
CallTaskAsync: After
CallTaskAsync: END

ということで、「await 付きの呼び出しをした際は、呼び出し元の関数の処理が続くのではなく、呼び出し元の関数から一度リターンする」 という挙動を覚えておこうと思います。

Discussion