C#でAsyncとAwaitを使用した制御フローを理解する

非同期で実際に待つ

で、このシリーズの前回のガイドたちは、の基本を見てましたasyncし、awaitC#でキーワードを。それらの構文と使用法のコツをつかんだら、非同期コードを書くことは実際には非常に楽しいことです。実際、コードが非同期であることを忘れることができるほど自然に感じます!多くの場合、これは利点です。細かな点を無視して、構築しているアプリケーションに集中することができます。しかし、遅かれ早かれ、非同期性がいかにトリッキーであるかを思い出させるいくつかの紛らわしい振る舞いに出くわすでしょう。これは、使用する際にボンネットの下に何が起こるかを理解することをこれらの瞬間だasyncawait重要になってきます。多くのことが起こっていることがわかりました。

ブロッキングコードと非ブロッキングコード

前のガイドから、asyncキーワードは実際には、に関するコンパイラのあいまいさを排除するための単なる方法であることを思い出してくださいawait。したがって、async/awaitアプローチについて話すとき、それは本当にawaitすべての面倒な作業を行うキーワードです。しかし、何awaitが行われるかを見る前に、何が行われないかについて話しましょう。

awaitキーワードは、現在のスレッドをブロックしません。それはどういう意味ですか?ブロッキングコードのいくつかの例を見てみましょう。

System.Threading.Thread.Sleep(1000);

上記のコードは、現在のスレッドの実行を1秒間ブロックします。アプリケーション内の他のスレッドは実行を継続できますが、現在のスレッドはスリープ操作が完了するまでまったく何もしません。それを説明する別の方法は、スレッドが同期的に待機することです。さて、別の例として、今回はタスク並列ライブラリから:

var httpClient = new HttpClient();
var myTask = httpClient.GetStringAsync("https://...");
var myString = myTask.GetAwaiter().GetResult();

上記のコードスニペットでは、.NETのHttpClientクラスがTaskインスタンスを返していますGetAwaiter().GetResult()が、ブロッキング呼び出しであるタスクを呼び出しています。繰り返しますが、これは同期です。GetResult操作によって返されたデータ(この例では要求された文字列データ)が返されるまで、現在のスレッドで実行は行われません。同様に、タスクのResultプロパティも、データが返されるまで同期的にブロックされます。

最後になりましたが、Waitブロックしているメソッドもあります。例:

myTask.Wait();

基になるタスクが非同期であっても、タスクのブロッキングメソッドまたはブロッキングプロパティを呼び出すと、実行はタスクが完了するのを待機しますが、同期的に実行されるため、待機中に現在のスレッドが完全に占有されます。したがって、上記のプロパティ/メソッドのいずれかを使用する場合は、それが実際に意図したことであることを確認してください。

awaitキーワードは、対照的に、ある非ブロック、現在のスレッドが待機中に他のことを行うために自由であることを意味し、。しかし、現在のスレッドは他に何をしているのでしょうか?

ユーザーの視点から待つ

非ブロッキング待機中にスレッドが何をするかについての質問に適切に答えるには、一歩下がって、アプリケーションユーザーの観点から一般的な非同期性について考えることが役立つ場合があります。例として:以前のガイドでは、画像をダウンロードしてぼかすアプリケーションのコードを見てきました。今回は、アプリケーションに2つのボタンと進行状況バーを備えたグラフィカルユーザーインターフェイスがあるとします。最初のボタンは、プログレスバーのアニメーションを表示しながら画像をダウンロードしてぼかします。2番目のボタンは、ぼかした画像の数をカウントし、ある種のダイアログでその数を示します。

ここで、あなたがユーザーであり、開発者がブロックまたは同期しているコードを使用したと想像してください。最初のボタンをクリックして画像をダウンロードしてぼかします。ダウンロードすると、アプリケーションがフリーズしたように見えます。プログレスバーがまったく表示されないか、アニメーションがフリーズして表示されます。おそらく、マウスカーソルが砂時計または風車に変わります。アプリケーションウィンドウを移動することもできません。ましてや、2番目のボタンをクリックすることもできません。アプリケーションを強制終了する以外に、ダウンロード/ぼかし操作が終了するのを待つしかありません。

同じアプリケーションがを使用するように作成されている場合のユーザーエクスペリエンスとは対照的ですawait。最初のボタンをクリックすると、進行状況バーがアニメーションとともに表示されます。待っている間に、アプリケーションウィンドウを移動することにしました。これにより、ウィンドウをドラッグするとスムーズに再描画されます。まだ待っているので、2番目のボタンをクリックして、その間に画像数を取得することにしました。やがて、画像数がダイアログに表示されます。最後に、ダウンロード/ぼかし操作が終了し、プログレスバーが非表示になります。

ご覧のとおり、ユーザーは、長時間実行される操作を待っている間に、元のスレッドが実行できるように十分に提供しました。事実awaitスレッドを解放して他のことを実行できるようにするということは、追加のユーザーアクションや入力に応答し続けることができることを意味します。ただし、グラフィカルユーザーインターフェイスがない場合でも、スレッドを解放することの利点を確認できます。このシリーズの以前のガイドで示したように、コンソールアプリケーションは、実行とは関係なく、テキストベースのダッシュボードの形式で進行状況を表示することもできます。そのアイデアを拡張して、キーボード入力を定期的にチェックすることができます。ASP.NETの場合、スレッドを解放するとスケーラビリティが向上する可能性があり、単一のサーバーが他の方法よりも多くの要求を同時に処理できるようになります。したがって、を使用してノンブロッキングコードを記述することは非常に有利awaitです。

呼び出し方法の観点から待つ

awaitこれでブロックされないことがわかりました。呼び出し元のスレッドが解放されます。しかし、この非ブロッキング動作は、呼び出し元のメソッドにどのように現れますか?次のコードについて考えてみます。

ShowDialogある種のメッセージアラートをユーザーに表示するメソッドがどこかに呼び出されたと仮定します。

void OnButtonClick()
{
  DownloadAndBlur("https://...jpg");
  ShowDialog("Success!");
}

async Task DownloadAndBlur(string url)
{
  await DownloadImage(...);  
  await BlurImage(...);
  await SaveImage(...);
}

このコードを実行すると、問題が発生します。ダウンロード/ぼかし操作が完了する前に、成功ダイアログが表示されます。これは重要なポイントを示してawaitます。使用するメソッド自体が待機されていない場合、呼び出されたメソッドの実行は、呼び出されたメソッドが完了する前に続行されます。詳細を確認するために、ログを追加してみましょう。

void OnButtonClick()
{
  Console.WriteLine("button clicked");
  DownloadAndBlur("https://...jpg");
  Console.WriteLine("about to show dialog");
  ShowDialog("Success!");
  Console.WriteLine("dialog shown");
}

async Task DownloadAndBlur(string url)
{
  Console.WriteLine("about to download");
  await DownloadImage(...);  
  Console.WriteLine("finished downloading, about to blur");
  await BlurImage(...);
  Console.WriteLine("finished blurring, about to save");
  await SaveImage(...);
  Console.WriteLine("finished saving");
}

出力は次のとおりです。

button clicked
about to download
about to show dialog
dialog shown
finished downloading, about to blur
finished blurring, about to save
finished saving

DownloadAndBlur最初は、との最初の遭遇まで、の実行は同期的に実行されることに注意してくださいawait。そのときの制御は、DownloadAndBlurすでに終了したかのように呼び出し元のメソッドに戻ります。

制御は呼び出し元のメソッドにすぐには戻らない場合がありますが、最初の機会に戻ることに注意してください。たとえば、ユーザーが適切なタイミングで別のボタンをクリックした場合、アプリケーションのメインスレッドはすでに他の何かでビジー状態になっている可能性があります。スレッドが次にアイドル状態になると、説明されているように、呼び出し元のメソッドで制御が再開されます。

もちろん、次のようにを使用してawait(忘れないでくださいasync)、上記の例を修正できOnButtonClickます。
 

async void OnButtonClick()
{
  Console.WriteLine("button clicked");
  await DownloadAndBlur("https://...jpg");
  Console.WriteLine("about to show dialog");
  ShowDialog("Success!");
  Console.WriteLine("dialog shown");
}

しかし、もっと大きな問題が残っています。すべての場合においてawait、メソッドの後のある時点で、メソッドは、いわば「ウェイクアップ」し、残りのコードを続行する必要があります。実行はメソッドの一部をどの程度正確に再開しますか?

待機呼び出し後にメソッドを再開する

その質問に答えるために、コールスタックをにログインしてみましょうDownloadAndBlur。そのための1つの方法は次のとおりです。

Console.WriteLine(new System.Diagnostics.StackTrace());

次のようなものが表示されます。

at OnButtonClick()
  at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
  at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AsyncStateMachineBox`1.MoveNext()
  at System.Threading.Tasks.AwaitTaskContinuation.RunOrScheduleAction(IAsyncStateMachineBox box, Boolean allowInlining)
  at System.Threading.Tasks.Task.RunContinuations(Object continuationObject)
  at System.Threading.Tasks.Task`1.TrySetResult(TResult result)
  at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.SetExistingTaskResult(TResult result)
  at System.Runtime.CompilerServices.AsyncTaskMethodBuilder.SetResult()
at DownloadAndBlur()
  at System.Threading.ExecutionContext.RunInternal...

ここでは、となどAsyncStateMachineBox.MoveNext()、コードで定義しなかった多くのメソッドが呼び出されていることに注意してくださいAsyncTaskMethodBuilder.SetResult。コンパイラが実行状態を追跡するために、私たちに代わって一連のコードを生成していることは明らかです。

生成されたコードの詳細はこのガイドの範囲外です(C#コンパイラとバージョンによって異なります)がgoto、タスク並列ライブラリメソッドと組み合わせてステートメントを使用するステートマシンが作成されていると言えば十分です。いくつかの例外とコンテキスト(つまりスレッド)の追跡を除きます。さらにawait詳しく知りたい場合は、C#に逆コンパイルできる.NETデコンパイラーに含まれている.NETアセンブリを調べてみてください。このように、あなたはすべての詳細を見ることができます!

Awaitを使用した例外処理制御フロー

例外処理に関する制御フローは、を使用した場合に予想されるとおりawaitですが、逆の場合はそうでないことを繰り返す価値があります。次のコードについて考えてみます。

async void OnButtonClick
{
  string imageUrl = null;
  try
  {
    DownloadAndBlur(imageUrl);
  }
  catch (Exception ex)
  {
    Console.WriteLine($"Exception: {ex}");
  }  
  Console.WriteLine("Done!");
}

async Task DownloadAndBlur(string url)
{
  if (url == null)
  {
    throw new ArgumentNullException(nameof(url));
  }  
  ...
}

出力にが含まれると予想される場合がありますException: ArgumentNullException。ただし、そうではないだけでなく、すべての例外で一時停止するデバッガーが接続されていない限り、例外が発生したことはまったくわかりません。awaitただし、この問題は使用時に発生しません。したがって、そうしない正当な理由がない限り、awaitすべての非同期メソッドに最適です。

しかし、「ファイアアンドフォーゲット」と呼ばれることが多いものについてはどうでしょうか。これはawait、非同期メソッドを使用したくない場合で、いつ終了するかについて特に心配する必要がない状況を指します。その場合は、少なくとも、発生する可能性のある例外をログに記録できる場所にを追加することを検討ContinueWithTaskContinuationOptions.OnlyOnFaultedてください。さらに良いことにawait、メソッドを先に進めますが、メソッドが最も外側のメソッドで行う最後のことを呼び出すようにします。そうすれば、の使用に伴う例外処理を利用しながら、他のコードの実行が延期されることはありませんawait

結論と次のステップ

アプリケーションの制御フローは、非同期の側面がある場合は常に少し注意が必要ですが、内部でどのようにawait機能するかを少し知っておくと非常に役立ちます。awaitただし、キーワードで最も重要なことは、それを使用することです。アプリケーションの動作を観察し、エッジケースのトラブルシューティングawaitを行うと、このガイドで紹介した制御フローが頭の中で強化され、この強力なツールに対する自信が高まります。

このシリーズのこれまでのところ、主にawaitI / O操作での使用について説明してきましたが、計算コストの高い操作には追加の何かが必要です。このシリーズ次のガイドでは、このトピックについて説明します。

リンク: https://www.pluralsight.com/guides/understand-control-flow-async-await

#csharp 

 

What is GEEK

Buddha Community

C#でAsyncとAwaitを使用した制御フローを理解する

C#でAsyncとAwaitを使用した制御フローを理解する

非同期で実際に待つ

で、このシリーズの前回のガイドたちは、の基本を見てましたasyncし、awaitC#でキーワードを。それらの構文と使用法のコツをつかんだら、非同期コードを書くことは実際には非常に楽しいことです。実際、コードが非同期であることを忘れることができるほど自然に感じます!多くの場合、これは利点です。細かな点を無視して、構築しているアプリケーションに集中することができます。しかし、遅かれ早かれ、非同期性がいかにトリッキーであるかを思い出させるいくつかの紛らわしい振る舞いに出くわすでしょう。これは、使用する際にボンネットの下に何が起こるかを理解することをこれらの瞬間だasyncawait重要になってきます。多くのことが起こっていることがわかりました。

ブロッキングコードと非ブロッキングコード

前のガイドから、asyncキーワードは実際には、に関するコンパイラのあいまいさを排除するための単なる方法であることを思い出してくださいawait。したがって、async/awaitアプローチについて話すとき、それは本当にawaitすべての面倒な作業を行うキーワードです。しかし、何awaitが行われるかを見る前に、何が行われないかについて話しましょう。

awaitキーワードは、現在のスレッドをブロックしません。それはどういう意味ですか?ブロッキングコードのいくつかの例を見てみましょう。

System.Threading.Thread.Sleep(1000);

上記のコードは、現在のスレッドの実行を1秒間ブロックします。アプリケーション内の他のスレッドは実行を継続できますが、現在のスレッドはスリープ操作が完了するまでまったく何もしません。それを説明する別の方法は、スレッドが同期的に待機することです。さて、別の例として、今回はタスク並列ライブラリから:

var httpClient = new HttpClient();
var myTask = httpClient.GetStringAsync("https://...");
var myString = myTask.GetAwaiter().GetResult();

上記のコードスニペットでは、.NETのHttpClientクラスがTaskインスタンスを返していますGetAwaiter().GetResult()が、ブロッキング呼び出しであるタスクを呼び出しています。繰り返しますが、これは同期です。GetResult操作によって返されたデータ(この例では要求された文字列データ)が返されるまで、現在のスレッドで実行は行われません。同様に、タスクのResultプロパティも、データが返されるまで同期的にブロックされます。

最後になりましたが、Waitブロックしているメソッドもあります。例:

myTask.Wait();

基になるタスクが非同期であっても、タスクのブロッキングメソッドまたはブロッキングプロパティを呼び出すと、実行はタスクが完了するのを待機しますが、同期的に実行されるため、待機中に現在のスレッドが完全に占有されます。したがって、上記のプロパティ/メソッドのいずれかを使用する場合は、それが実際に意図したことであることを確認してください。

awaitキーワードは、対照的に、ある非ブロック、現在のスレッドが待機中に他のことを行うために自由であることを意味し、。しかし、現在のスレッドは他に何をしているのでしょうか?

ユーザーの視点から待つ

非ブロッキング待機中にスレッドが何をするかについての質問に適切に答えるには、一歩下がって、アプリケーションユーザーの観点から一般的な非同期性について考えることが役立つ場合があります。例として:以前のガイドでは、画像をダウンロードしてぼかすアプリケーションのコードを見てきました。今回は、アプリケーションに2つのボタンと進行状況バーを備えたグラフィカルユーザーインターフェイスがあるとします。最初のボタンは、プログレスバーのアニメーションを表示しながら画像をダウンロードしてぼかします。2番目のボタンは、ぼかした画像の数をカウントし、ある種のダイアログでその数を示します。

ここで、あなたがユーザーであり、開発者がブロックまたは同期しているコードを使用したと想像してください。最初のボタンをクリックして画像をダウンロードしてぼかします。ダウンロードすると、アプリケーションがフリーズしたように見えます。プログレスバーがまったく表示されないか、アニメーションがフリーズして表示されます。おそらく、マウスカーソルが砂時計または風車に変わります。アプリケーションウィンドウを移動することもできません。ましてや、2番目のボタンをクリックすることもできません。アプリケーションを強制終了する以外に、ダウンロード/ぼかし操作が終了するのを待つしかありません。

同じアプリケーションがを使用するように作成されている場合のユーザーエクスペリエンスとは対照的ですawait。最初のボタンをクリックすると、進行状況バーがアニメーションとともに表示されます。待っている間に、アプリケーションウィンドウを移動することにしました。これにより、ウィンドウをドラッグするとスムーズに再描画されます。まだ待っているので、2番目のボタンをクリックして、その間に画像数を取得することにしました。やがて、画像数がダイアログに表示されます。最後に、ダウンロード/ぼかし操作が終了し、プログレスバーが非表示になります。

ご覧のとおり、ユーザーは、長時間実行される操作を待っている間に、元のスレッドが実行できるように十分に提供しました。事実awaitスレッドを解放して他のことを実行できるようにするということは、追加のユーザーアクションや入力に応答し続けることができることを意味します。ただし、グラフィカルユーザーインターフェイスがない場合でも、スレッドを解放することの利点を確認できます。このシリーズの以前のガイドで示したように、コンソールアプリケーションは、実行とは関係なく、テキストベースのダッシュボードの形式で進行状況を表示することもできます。そのアイデアを拡張して、キーボード入力を定期的にチェックすることができます。ASP.NETの場合、スレッドを解放するとスケーラビリティが向上する可能性があり、単一のサーバーが他の方法よりも多くの要求を同時に処理できるようになります。したがって、を使用してノンブロッキングコードを記述することは非常に有利awaitです。

呼び出し方法の観点から待つ

awaitこれでブロックされないことがわかりました。呼び出し元のスレッドが解放されます。しかし、この非ブロッキング動作は、呼び出し元のメソッドにどのように現れますか?次のコードについて考えてみます。

ShowDialogある種のメッセージアラートをユーザーに表示するメソッドがどこかに呼び出されたと仮定します。

void OnButtonClick()
{
  DownloadAndBlur("https://...jpg");
  ShowDialog("Success!");
}

async Task DownloadAndBlur(string url)
{
  await DownloadImage(...);  
  await BlurImage(...);
  await SaveImage(...);
}

このコードを実行すると、問題が発生します。ダウンロード/ぼかし操作が完了する前に、成功ダイアログが表示されます。これは重要なポイントを示してawaitます。使用するメソッド自体が待機されていない場合、呼び出されたメソッドの実行は、呼び出されたメソッドが完了する前に続行されます。詳細を確認するために、ログを追加してみましょう。

void OnButtonClick()
{
  Console.WriteLine("button clicked");
  DownloadAndBlur("https://...jpg");
  Console.WriteLine("about to show dialog");
  ShowDialog("Success!");
  Console.WriteLine("dialog shown");
}

async Task DownloadAndBlur(string url)
{
  Console.WriteLine("about to download");
  await DownloadImage(...);  
  Console.WriteLine("finished downloading, about to blur");
  await BlurImage(...);
  Console.WriteLine("finished blurring, about to save");
  await SaveImage(...);
  Console.WriteLine("finished saving");
}

出力は次のとおりです。

button clicked
about to download
about to show dialog
dialog shown
finished downloading, about to blur
finished blurring, about to save
finished saving

DownloadAndBlur最初は、との最初の遭遇まで、の実行は同期的に実行されることに注意してくださいawait。そのときの制御は、DownloadAndBlurすでに終了したかのように呼び出し元のメソッドに戻ります。

制御は呼び出し元のメソッドにすぐには戻らない場合がありますが、最初の機会に戻ることに注意してください。たとえば、ユーザーが適切なタイミングで別のボタンをクリックした場合、アプリケーションのメインスレッドはすでに他の何かでビジー状態になっている可能性があります。スレッドが次にアイドル状態になると、説明されているように、呼び出し元のメソッドで制御が再開されます。

もちろん、次のようにを使用してawait(忘れないでくださいasync)、上記の例を修正できOnButtonClickます。
 

async void OnButtonClick()
{
  Console.WriteLine("button clicked");
  await DownloadAndBlur("https://...jpg");
  Console.WriteLine("about to show dialog");
  ShowDialog("Success!");
  Console.WriteLine("dialog shown");
}

しかし、もっと大きな問題が残っています。すべての場合においてawait、メソッドの後のある時点で、メソッドは、いわば「ウェイクアップ」し、残りのコードを続行する必要があります。実行はメソッドの一部をどの程度正確に再開しますか?

待機呼び出し後にメソッドを再開する

その質問に答えるために、コールスタックをにログインしてみましょうDownloadAndBlur。そのための1つの方法は次のとおりです。

Console.WriteLine(new System.Diagnostics.StackTrace());

次のようなものが表示されます。

at OnButtonClick()
  at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
  at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AsyncStateMachineBox`1.MoveNext()
  at System.Threading.Tasks.AwaitTaskContinuation.RunOrScheduleAction(IAsyncStateMachineBox box, Boolean allowInlining)
  at System.Threading.Tasks.Task.RunContinuations(Object continuationObject)
  at System.Threading.Tasks.Task`1.TrySetResult(TResult result)
  at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.SetExistingTaskResult(TResult result)
  at System.Runtime.CompilerServices.AsyncTaskMethodBuilder.SetResult()
at DownloadAndBlur()
  at System.Threading.ExecutionContext.RunInternal...

ここでは、となどAsyncStateMachineBox.MoveNext()、コードで定義しなかった多くのメソッドが呼び出されていることに注意してくださいAsyncTaskMethodBuilder.SetResult。コンパイラが実行状態を追跡するために、私たちに代わって一連のコードを生成していることは明らかです。

生成されたコードの詳細はこのガイドの範囲外です(C#コンパイラとバージョンによって異なります)がgoto、タスク並列ライブラリメソッドと組み合わせてステートメントを使用するステートマシンが作成されていると言えば十分です。いくつかの例外とコンテキスト(つまりスレッド)の追跡を除きます。さらにawait詳しく知りたい場合は、C#に逆コンパイルできる.NETデコンパイラーに含まれている.NETアセンブリを調べてみてください。このように、あなたはすべての詳細を見ることができます!

Awaitを使用した例外処理制御フロー

例外処理に関する制御フローは、を使用した場合に予想されるとおりawaitですが、逆の場合はそうでないことを繰り返す価値があります。次のコードについて考えてみます。

async void OnButtonClick
{
  string imageUrl = null;
  try
  {
    DownloadAndBlur(imageUrl);
  }
  catch (Exception ex)
  {
    Console.WriteLine($"Exception: {ex}");
  }  
  Console.WriteLine("Done!");
}

async Task DownloadAndBlur(string url)
{
  if (url == null)
  {
    throw new ArgumentNullException(nameof(url));
  }  
  ...
}

出力にが含まれると予想される場合がありますException: ArgumentNullException。ただし、そうではないだけでなく、すべての例外で一時停止するデバッガーが接続されていない限り、例外が発生したことはまったくわかりません。awaitただし、この問題は使用時に発生しません。したがって、そうしない正当な理由がない限り、awaitすべての非同期メソッドに最適です。

しかし、「ファイアアンドフォーゲット」と呼ばれることが多いものについてはどうでしょうか。これはawait、非同期メソッドを使用したくない場合で、いつ終了するかについて特に心配する必要がない状況を指します。その場合は、少なくとも、発生する可能性のある例外をログに記録できる場所にを追加することを検討ContinueWithTaskContinuationOptions.OnlyOnFaultedてください。さらに良いことにawait、メソッドを先に進めますが、メソッドが最も外側のメソッドで行う最後のことを呼び出すようにします。そうすれば、の使用に伴う例外処理を利用しながら、他のコードの実行が延期されることはありませんawait

結論と次のステップ

アプリケーションの制御フローは、非同期の側面がある場合は常に少し注意が必要ですが、内部でどのようにawait機能するかを少し知っておくと非常に役立ちます。awaitただし、キーワードで最も重要なことは、それを使用することです。アプリケーションの動作を観察し、エッジケースのトラブルシューティングawaitを行うと、このガイドで紹介した制御フローが頭の中で強化され、この強力なツールに対する自信が高まります。

このシリーズのこれまでのところ、主にawaitI / O操作での使用について説明してきましたが、計算コストの高い操作には追加の何かが必要です。このシリーズ次のガイドでは、このトピックについて説明します。

リンク: https://www.pluralsight.com/guides/understand-control-flow-async-await

#csharp