ミルク蒼屋のチラシ

Colloid(コロイド)が何か色々と残したりするブログです

async/awaitとTask.Runの動作を実際に確認してみる

前置き

当記事はC# Advent Calendar 2021 その2 20日目の記事になります。

qiita.com

本題

最近仕事で

f:id:Colloid:20211219153659p:plain

となる時がよくあるんですよね。自分の中では理解しているつもりで、こうだろうと仮説を立てて実装し、出力されたログをみているとどうも自分の中の理解と少し違う動きをしているっぽいぞ。と。今回はそのよくわかんないな~という部分を実際にコンソールアプリに実装してみて動作をみてみようという内容です。
こういうのは自分でやろうとしないと身につかないのが私なので、Advent Calendarを利用して手を動かそうという経緯です。なので自分への備忘録も兼ねて残します。

よくわかんないという所

ここがよくわかんないよ~~~~という所を羅列してみる

  • async/awaitのawaitって待ってくれている認識だけど、本当に待っているの?

  • ただのメソッド内からasync/await処理を実行すると、処理の順番どうなっているの

  • 複数あるasync/awaitを実行するとどういう順番で動作するの

主にこの3つでしょうか。具体的には実行順の認識があやしいという所ですかね。

書いてみる

とりあえず、上記のよくわからんリストはどういう事なのかを具体的にコードに起こしてみます。

async/awaitの振る舞いについて確認してみる

上記の思った背景として、ちゃんとawaitと書いているのに待たずに次の処理走っているな感ある動きをログでみてしまった所です。多くのサイトや公式ドキュメントをみると、ちゃんと待つよとあるので待つんだろうと思うけど、async/awaitの処理を再度確認ということで念の為確認する。

docs.microsoft.com

今回文章の元ネタに2chのくぅ疲コピペを使ってみます。

くぅ~疲れましたw これにて完結です! 実は、ネタレスしたら代行の話を持ちかけられたのが始まりでした 本当は話のネタなかったのですが← ご厚意を無駄にするわけには行かないので流行りのネタで挑んでみた所存ですw 以下、まどか達のみんなへのメッセジをどぞ

まどか「みんな、見てくれてありがとう ちょっと腹黒なところも見えちゃったけど・・・気にしないでね!」

さやか「いやーありがと! 私のかわいさは二十分に伝わったかな?」

マミ「見てくれたのは嬉しいけどちょっと恥ずかしいわね・・・」

京子「見てくれありがとな! 正直、作中で言った私の気持ちは本当だよ!」

ほむら「・・・ありがと」ファサ

では、

まどか、さやか、マミ、京子、ほむら、俺「皆さんありがとうございました!」

まどか、さやか、マミ、京子、ほむら「って、なんで俺くんが!? 改めまして、ありがとうございました!」

本当の本当に終わり

くぅ〜疲れましたw - VIP Wiki*

順番がわかりやすいかなって思ってこれにしました。
.NETは.NET6を使っています。コードは実現できればいいやの精神で適当な所があります。

Console.WriteLine("くぅ~疲れましたw これにて完結です!");
Console.WriteLine(await GetMaeokiAsync());
Console.WriteLine(await GetMadokaAsync());
Console.WriteLine(GetSayaka());

Console.WriteLine(@"本当の本当に終わり");

async Task<string> GetMaeokiAsync()
{
    await Task.Delay(1000);
    Console.WriteLine(@"
実は、ネタレスしたら代行の話を持ちかけられたのが始まりでした
本当は話のネタなかったのですが←
ご厚意を無駄にするわけには行かないので流行りのネタで挑んでみた所存ですw ");
    await Task.Delay(1000);
    return "以下、まどか達のみんなへのメッセジをどぞ";
}

async Task<string> GetMadokaAsync()
{
    await Task.Delay(1000);
    Console.WriteLine(@"まどか「みんな、見てくれてありがとう");
    await Task.Delay(1000);
    return "ちょっと腹黒なところも見えちゃったけど・・・気にしないでね!」";
}

string GetSayaka()
{
    Task.Delay(1000);
    Console.WriteLine(@"さやか「いやーありがと!");
    Task.Delay(1000);
    return "私のかわいさは二十分に伝わったかな?」";
}

実行結果 f:id:Colloid:20211219225759p:plain

当たり前ですけどちゃんと待ってくれているんですよね。多くのサイトに解説されている通り、async/awaitが書かれていれば待ってくれている。
実行順もくぅ疲はさておき、上から順番にGetMaeokiAsync()、GetMadokaAsync()、GetSayaka()の順で実行される。想定通りである。あれ?では私の見たログは一体何なんだったのか。

普通のメソッドから呼び出してみる

実装対象がかつて古いバージョンで書かれていた場合、async/await化するのに苦労することがあると個人的に思う。例えばここをasync/awaitしたいけど普通のメソッドだから呼び方わからん。。。とか。
こういうときにTask.Runとかでゴリ押しでasync/awaitメソッドを呼び出すとかできる。たぶんその処理の場合、順番がわけわからんという状態で、上から下へ流れているつもりが想定通りに動かない状態なのかなと思う。ということで、.NETのバージョンを落として同じような状況を試してみる*1。コードはぶっちゃけ試したいことを試しているので適当です。

.NETのバージョンは.NET Framework 4.8です。残りを追加して、素直に上から順に実行するとコピペの通りになるっしょ!みたいな安直な考えの順番(※あえてこうしています)で実装しています。待機時間も統一しました。

    public class Program
    {
       public static void Main(string[] args)
        {
            Console.WriteLine("くぅ~疲れましたw これにて完結です!");

            var p = new Program();
            Task.Run(async () => 
            {
                var maeoki = await p.GetMaeokiAsync();
                var madoka = await p.GetMadokaAsync();
                var sayaka = p.GetSayaka();

                Console.WriteLine(maeoki);
                Console.WriteLine(madoka);
                Console.WriteLine(sayaka);
            });

            Task.Run(async () =>
            {
                var mamikyoko = await p.GetMamiKyokoAsync();
                var homuhomu = await p.GetHomuHomuAsync();
                Console.WriteLine(mamikyoko);
                Console.WriteLine(homuhomu);
            });
            Console.WriteLine("では、");
            Console.WriteLine(p.GetThanks());

            Console.WriteLine("終");

            Task.Run(async () =>
            {
                var rethanks = await p.GetReThanksAsync();
                Console.WriteLine(rethanks);
            });

            Console.WriteLine("本当の本当に終わり");

            Console.ReadKey();
        }

        async Task<string> GetMaeokiAsync()
        {
            await Task.Delay(1000);
            Console.WriteLine(@"
実は、ネタレスしたら代行の話を持ちかけられたのが始まりでした
本当は話のネタなかったのですが←
ご厚意を無駄にするわけには行かないので流行りのネタで挑んでみた所存ですw ");
            await Task.Delay(1000);
            return "以下、まどか達のみんなへのメッセジをどぞ";
        }

        async Task<string> GetMadokaAsync()
        {
            await Task.Delay(1000);
            Console.WriteLine(@"まどか「みんな、見てくれてありがとう");
            await Task.Delay(1000);
            return "ちょっと腹黒なところも見えちゃったけど・・・気にしないでね!」";
        }

        string GetSayaka()
        {
            Task.Delay(1000);
            Console.WriteLine(@"さやか「いやーありがと!");
            Task.Delay(1000);
            return "私のかわいさは二十分に伝わったかな?」";
        }

        async Task<string> GetMamiKyokoAsync()
        {
            await Task.Delay(1000);
            Console.WriteLine(@"マミ「見てくれたのは嬉しいけどちょっと恥ずかしいわね・・・」 ");
            await Task.Delay(1000);
            return @"京子「見てくれありがとな!
正直、作中で言った私の気持ちは本当だよ!」 ";
        }

        async Task<string> GetHomuHomuAsync()
        {
            await Task.Delay(1000);
            return @"ほむら「・・・ありがと」ファサ ";
        }

        string GetThanks()
        {
            Task.Delay(1000);
            return "まどか、さやか、マミ、京子、ほむら、俺「皆さんありがとうございました!」 ";
        }

        async Task<string> GetReThanksAsync()
        {
            await Task.Delay(1000);
            Console.WriteLine("まどか、さやか、マミ、京子、ほむら「って、なんで俺くんが!? ");
            await Task.Delay(1000);
            return @"改めまして、ありがとうございました!」 ";
        }
    }

実行結果
f:id:Colloid:20211220000610p:plain 早々にコピペが終わりましたね。おそらく冒頭の上から下に動いている(=想定している動作)と思っていたら異なっていたという原因がこれだったんだろうなと思っています。
何気ないmainメソッドにTaskが3つあり、その中で非同期処理を呼ぶ。なのでmainのTask以外の処理を早々に終わらせつつ、別スレッドでは同時並行でメッセージを呼び出すだけのメソッドを処理してますね。なのでコピペの順番がわりとばらけた感じです。
うーんなんという非同期処理、マルチタスクな処理…。 f:id:Colloid:20211220000911p:plain
タスクウィンドウにも3つのタスクが待機中とあるので、Mainの中に3つのタスクが非同期で処理しているというのが目に見えてわかりますね。
async/awaitの実行順が違うな???というところはおそらくこれが原因で、async/awaitがついていないメソッドから呼び出す時に無理にTasuk.Runで呼ぶと、他のメソッドのシーケンスを考慮できずで順序が想定していた通りではない順序になるんだろうなと思いました。こういう時は頑張ってTask化してasync/awaitでやるのが良いのかもしれません。

所感

調査してみての感想としては、改めて非同期処理やマルチスレッドな処理とはなんなのかというのが知れたなと思いました。そして実装対象の規模が大きく、一部async/await化している場合だと上記のような普通のメソッドから呼び出すときの影響範囲を把握するのが難しいと改めて調査して思いました。こういう時は地道にコードを読んで解釈してシーケンスを理解するしかないですね(泣)
またプログラムを始めたての人が非同期処理をやるという段階になるときに、async/awaitを初っ端書くとそれはもう同期処理のように見えるので、非同期でマルチスレッドで処理を体感してみたい!とあればこのように普通のメソッドからTaskを呼び出すという事をやればわかりやすいなと個人的に思いました。

最後に

.NET6でデフォルトのプログラムテンプレートが変わるというのは小耳に挟んでいましたが、いざ触り始めるとテンパりますねw
お陰様で慣れるのに少し時間がかかってしまいましたorz
現場からは以上です。

*1:.NET6 にテンパりすぎてMainメソッドをノーマルなメソッドと非同期メソッドの切り替えがわからなくて泣く泣くVerを落としましたorz