多分わかりやすいC#の非同期処理その2 〜 asyncとawait 〜

◆ Live配信スケジュール ◆
サイオステクノロジーでは、Microsoft MVPの武井による「わかりみの深いシリーズ」など、定期的なLive配信を行っています。
⇒ 詳細スケジュールはこちらから
⇒ 見逃してしまった方はYoutubeチャンネルをご覧ください
【3/22開催】テックブログを書こう!アウトプットのススメ
1年で100本ブログを出した新米エンジニアがPV数が伸びなくてもTech Blogを書き続ける理由とは?
https://tech-lab.connpass.com/event/312805/

【4/18開催】VSCode Dev Containersで楽々開発環境構築祭り〜Python/Reactなどなど〜
Visual Studio Codeの拡張機能であるDev Containersを使ってReactとかPythonとかSpring Bootとかの開発環境をラクチンで構築する方法を紹介するイベントです。
https://tech-lab.connpass.com/event/311864/

こんにちは、サイオステクノロジー技術部 武井(Twitter:@noriyukitakei)です。今回は前回に引き続き、C#の非同期処理について書きたいと思います。

今回は前回のThreadとTaskよりももっと大変なasyncとawaitです。

※他C#関連の以下の記事もご参考下さい。

C#関連の記事一覧を表示

asyncとawaitとは?

asyncとawaitとは以下の効用を持っております。

  • asyncをつけたメソッドは、そのメソッドの中で呼び出すメソッドの前にawaitをつけることができる。
  • awaitをつけたメソッド(メソッドAとします)の実行が開始されると、一度そのメソッドを抜けて、呼び出し元のメソッド(メソッドBとします)に戻ります。この間、裏でメソッドAは実行中であり、メソッドAの実行が完了すると、再びメソッドAに処理が戻ります。

さっぱりわからないと思います。私も上記のような説明を目にしたことがありますが、さっぱりわかりませんでした。

これから、asyncとawaitについて多分わかりやすく解説していくつもりですが、このasyncとawaitの最大のメリットは、

同期処理を書くような感覚で、非同期処理を書くことができる。

です。

非同期処理は、async、awaitを使わないと、きちんとスレッドを意識しないと書くことが出来ません。しかしながら、async、awaitを使えば、スレッド周りのことはすべてC#がやってくれます。ソースコードからスレッドを隠蔽できます。(実際は裏でやっていることはスレッドを生成しているのですが)

とある同期処理

では、早速説明していきます。話がややこしいので、いきなり核心から責めず、ちょっとだけ遠回りをします。まず、以下のような同期処理を考えてみます。

class Program
{
    static void Main(string[] args)
    {   
        string result = HeavyMethod1();

        HeavyMethod2();

        Console.WriteLine(result);

        Console.ReadLine();
    }   

    static string HeavyMethod1()
    {   
        Console.WriteLine("すごく重い処理その1(´・ω・`)はじまり"); 
        Thread.Sleep(5000);
        Console.WriteLine("すごく重い処理その1(´・ω・`)おわり"); 
        return "hoge";
    }   

    static void HeavyMethod2()
    {   
        Console.WriteLine("すごく重い処理その2(´・ω・`)はじまり"); 
        Thread.Sleep(3000);
        Console.WriteLine("すごく重い処理その2(´・ω・`)おわり"); 
    }   
}

このプログラムは、hogeという文字列を返すHeavyMethod1を実行した後に、同じく重い処理を行うHeavyMethod2を実行し、最後にHeavyMethod1の戻り値であるhogeをコンソールに表示するという単純なものです。

ここで、HeavyMethod1を非同期にすることを考えてみます。

Threadを使う場合

ということで、先程の同期処理のHeavyMethod1をThreadで非同期にしてみました。

class Program
{
    static void Main(string[] args)
    {   
        // メインスレッドからアクセスできる変数を定義します。これに結果を格納します。
        string result = ""; 
        Thread thread = new Thread(new ThreadStart(() =>
        {   
            // メインスレッドとは異なるスレットでHeavyMethodを実行して、
            // 結果(hoge)をresultに格納します。
            result = HeavyMethod1();
        }));

        // スレッドを開始します。
        thread.Start();

        // HeavyMethod2を実行します。
        HeavyMethod2();

        // Joinメソッドを使うとスレッドが終了するまで、これより先のコードに
        // 進まないようになります。
        thread.Join();

        // 結果をコンソールに表示します。
        Console.WriteLine(result);

        Console.ReadLine();
    }   

    static String HeavyMethod1()
    {   
        Console.WriteLine("すごく重い処理その1(´・ω・`)はじまり"); 
        Thread.Sleep(5000);
        Console.WriteLine("すごく重い処理その1(´・ω・`)おわり"); 
        return "hoge";
    }   

    static void HeavyMethod2()
    {   
        Console.WriteLine("すごく重い処理その2(´・ω・`)はじまり"); 
        Thread.Sleep(3000);
        Console.WriteLine("すごく重い処理その2(´・ω・`)おわり"); 
    }   
}

いかにもスレッドを意識した大変なソースコードになりました。Threadを使った場合は、わざわざメインスレッドでresultという変数を定義して、スレッドの中でそれを使っています。しかもJoinメソッドでスレッドの終了を待機しています。いかにもスレッドであることを意識したコードです。

async、awaitを使う場合

Threadを使った非同期処理をasync、awaitで書き直してみます。

class Program
{
    static async Task Main(string[] args)
    {   
        Task<string> task = HeavyMethod1();

        HeavyMethod2();

        Console.WriteLine(task.Result);

        Console.ReadLine();
    }   

    static async Task<string> HeavyMethod1()
    {   
        Console.WriteLine("すごく重い処理その1(´・ω・`)はじまり"); 
        await Task.Delay(5000);
        Console.WriteLine("すごく重い処理その1(´・ω・`)おわり"); 
        return "hoge";
    }   

    static void HeavyMethod2()
    {   
        Console.WriteLine("すごく重い処理その2(´・ω・`)はじまり"); 
        Thread.Sleep(3000);
        Console.WriteLine("すごく重い処理その2(´・ω・`)おわり"); 
    }   
}

同期のコードの差を比較したのが以下の図になります。同期のコードの構造を崩すことなく、たった4箇所の変更だけで、非同期のコードが出来上がってしまいました。

同期とasync、awaitの差

async、awaitの実行順序

では、先程のasync、awaitを使ったコードがどのように実行されていくのかをご説明したいと思います。


■その1:Mainメソッドの実行

スライド1

まず、何はともあれMainメソッドの実行です。同期的なコードと比較すると、修飾子が変わってます。static voidがstatic async Taskになっています。これはMainメソッドの中でasyncメソッドを使うおまじないと思っておいてもらえればよいかと思います。ちなみにこれが使えるのはC#7.1からです。


■その2:HeavyMethod1メソッドの実行

スライド2

非同期メソッドHeavyMethod1を実行します。次で説明しますが、非同期メソッドには、修飾子asyncをつけて、戻り値はTask型もしくはTask型のジェネリックにするという決まりがあります。Taskについては前回の記事を参考にして下さい。Taskとは非同期メソッドを実行するための一つの手段で、非同期処理の戻り値にTask型を返すことにより、非同期メソッドの状態や戻り値などを知ることができるものでした。

この非同期メソッドは、hogeというstringを返すので、Task型なのです。


■その3:引き続きHeavyMethod1メソッドの実行

スライド3

HeavyMethod1メソッドに入りました。先程の同期的なコードと違い、メソッドの修飾子にasyncが付いています。これは非同期メソッドはasyncを付けるという決まりであり、asyncを付けることによってawait句が使えるようになります。これについては、次で説明します。

そして、asyncメソッドの戻り値は、必ずTask型もしくはTask型のジェネリックとします。

Task型ということは、前回の記事でも書きましたように、非同期メソッドの状態や戻り値などを知ることが出来ます。


■その4:awaitが付いたメソッドの実行

スライド4

ここがキモなのですが、非同期にしたい重い処理にはawaitをつけて実行します。awaitは戻り値がTask型のメソッドにしか付けられません。そういう決まりです。なので、ここでは、Thread.SleepをTask.Delayに書き直しています。

Task.Delayは「引数で指定した秒数を待ち、その実行状態をTask型で返す非同期メソッド」です。

たぶん、こう書いても同じだと思います。

Task.Run(() => {
    Thread.Sleep(3000);
});

そして、Task.Delayは先程も書いたように非同期メソッドです。そのままTask.Delayと実行すると、非同期で実行されるので、待ってくれません。「XXX秒待つ」という処理を非同期で実行するので、そのまま何もせずスルーです。実際は、もう一つスレッドが起動し、そこで非同期で「XXX秒待つ」という処理をしていますが、それを非同期で実施したら、実質上は何もしないのと同じになってしまいます。

そこで、awaitにはもう一つの効用がありまして、Task型を返す非同期メソッドが終了するまで待ってくれるのです。なので、await Task.Delay(3000)は、次の「Console.WriteLine(“すごく重い処理その1(´・ω・`)おわり”);」をすぐに実施せず、一旦3000ミリ秒待ちます。

そして、awaitが付いているメソッドが実行されると、一旦そのメソッドを抜けます。つまりMainメソッドに戻ります。これもawaitの効用です。


■その5:HeavyMethod2の実行

スライド5

awaitが付いたメソッドが実行されたので、一旦HeavyMethod1から抜けて、Mainメソッドに戻り、HeavyMethod2を実行します。でも、この間にもTask.Delay(3000)はバックグラウンド、つまり別スレッドにて実行中です。別スレッドで実行中なのですが、全くスレッドに関するコード(thread.startなど)を書いていません。await付けただけです。ほら、async、awaitを使うと同期的なコードの書き方なのに、非同期的な動きをしますよね。


■その6:再びHeavyMethod1の実行

スライド6

そして、先程から別スレッドで動作しているHeavyMethod1のTask.Delay(3000)(重い処理に相当します)が終了すると、再びHeavyMethod1に戻ってきて、以降の処理を再開します。


■その7:HeavyMethod1の戻り値の取得

スライド7

asyncメソッドの戻り値はTask型です。となると、前回の記事でも書きましたように、Resultプロパティで戻り値を取得できます。

これで、無事にHeavyMethod1を非同期で処理できました。

まとめ

いかがでしたでしょうか?Thread使うとずいぶんと入り組んだコードになってしまいますが、async、awaitを使うと、ずいぶんスッキリしたコードになると思いませんか?スレッドというものを意識せず、非同期にしたいメソッドにasyncを付けるだけという、非常に同期的なコードに近い書き方で非同期処理を実現出来ます。async、awaitをどんどん使っていきましょう

以下も合わせてご参考頂けますと幸いです。

多分わかりやすいC#の非同期処理その1 〜 ThreadとTask 〜

他C#関連の以下の記事もありますので、是非見て頂ければと思います!!

C#関連の記事一覧を表示

アバター画像
About 武井 宜行 267 Articles
Microsoft MVP for Azure🌟「最新の技術を楽しくわかりやすく」をモットーにブログtech-lab.sios.jp)で情報を発信🎤得意分野はAzureによるクラウドネイティブな開発(Javaなど)💻「世界一わかりみの深いクラウドネイティブ on Azure」の動画を配信中📹 https://t.co/OMaJYb3pRN
ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

33人がこの投稿は役に立ったと言っています。


ご覧いただきありがとうございます。
ブログの最新情報はSNSでも発信しております。
ぜひTwitterのフォロー&Facebookページにいいねをお願い致します!



>> 雑誌等の執筆依頼を受付しております。
   ご希望の方はお気軽にお問い合わせください!

1 Comment

  1. 非常に理解に役立ちました!
    ありがとうございます。
    1点気になったのは、メソッドasync Task HeavyMethod1()内のTask.Delay(5000)の引数がいつのまにか3000に変わってました。
    そのため、HeavyMethod2のことを言っているかと一瞬勘違いしそうになりました。
    訂正いただけたら幸いです。

Leave a Reply

Your email address will not be published.


*


質問はこちら 閉じる