こんにちは、サイオステクノロジー技術部 武井(Twitter:@noriyukitakei)です。今回は前回に引き続き、C#の非同期処理について書きたいと思います。
今回は前回のThreadとTaskよりももっと大変なasyncとawaitです。
- 多分わかりやすいC#の非同期処理その1 〜 ThreadとTask 〜
- 今回はコチラ → 多分わかりやすいC#の非同期処理その2 〜 asyncとawait 〜
※他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を使ったコードがどのように実行されていくのかをご説明したいと思います。
■その1:Mainメソッドの実行
まず、何はともあれMainメソッドの実行です。同期的なコードと比較すると、修飾子が変わってます。static voidがstatic async Taskになっています。これはMainメソッドの中でasyncメソッドを使うおまじないと思っておいてもらえればよいかと思います。ちなみにこれが使えるのはC#7.1からです。
■その2:HeavyMethod1メソッドの実行
非同期メソッドHeavyMethod1を実行します。次で説明しますが、非同期メソッドには、修飾子asyncをつけて、戻り値はTask型もしくはTask型のジェネリックにするという決まりがあります。Taskについては前回の記事を参考にして下さい。Taskとは非同期メソッドを実行するための一つの手段で、非同期処理の戻り値にTask型を返すことにより、非同期メソッドの状態や戻り値などを知ることができるものでした。
この非同期メソッドは、hogeというstringを返すので、Task型なのです。
■その3:引き続きHeavyMethod1メソッドの実行
HeavyMethod1メソッドに入りました。先程の同期的なコードと違い、メソッドの修飾子にasyncが付いています。これは非同期メソッドはasyncを付けるという決まりであり、asyncを付けることによってawait句が使えるようになります。これについては、次で説明します。
そして、asyncメソッドの戻り値は、必ずTask型もしくはTask型のジェネリックとします。
Task型ということは、前回の記事でも書きましたように、非同期メソッドの状態や戻り値などを知ることが出来ます。
■その4:awaitが付いたメソッドの実行
ここがキモなのですが、非同期にしたい重い処理には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の実行
awaitが付いたメソッドが実行されたので、一旦HeavyMethod1から抜けて、Mainメソッドに戻り、HeavyMethod2を実行します。でも、この間にもTask.Delay(3000)はバックグラウンド、つまり別スレッドにて実行中です。別スレッドで実行中なのですが、全くスレッドに関するコード(thread.startなど)を書いていません。await付けただけです。ほら、async、awaitを使うと同期的なコードの書き方なのに、非同期的な動きをしますよね。
■その6:再びHeavyMethod1の実行
そして、先程から別スレッドで動作しているHeavyMethod1のTask.Delay(3000)(重い処理に相当します)が終了すると、再びHeavyMethod1に戻ってきて、以降の処理を再開します。
■その7:HeavyMethod1の戻り値の取得
asyncメソッドの戻り値はTask型です。となると、前回の記事でも書きましたように、Resultプロパティで戻り値を取得できます。
これで、無事にHeavyMethod1を非同期で処理できました。
まとめ
いかがでしたでしょうか?Thread使うとずいぶんと入り組んだコードになってしまいますが、async、awaitを使うと、ずいぶんスッキリしたコードになると思いませんか?スレッドというものを意識せず、非同期にしたいメソッドにasyncを付けるだけという、非常に同期的なコードに近い書き方で非同期処理を実現出来ます。async、awaitをどんどん使っていきましょう
以下も合わせてご参考頂けますと幸いです。
多分わかりやすいC#の非同期処理その1 〜 ThreadとTask 〜
他C#関連の以下の記事もありますので、是非見て頂ければと思います!!
非常に理解に役立ちました!
ありがとうございます。
1点気になったのは、メソッドasync Task HeavyMethod1()内のTask.Delay(5000)の引数がいつのまにか3000に変わってました。
そのため、HeavyMethod2のことを言っているかと一瞬勘違いしそうになりました。
訂正いただけたら幸いです。