次の方法で共有


Async/Await

非同期プログラミングのベスト プラクティス

Stephen Cleary

 

最近は、Microsoft .NET Framework 4.5 でサポートされる新しい async と await に関する情報がたくさん公開されています。今回は、非同期プログラミングについて学習する "第 2 段階" にしようと思っています。そのため、非同期プログラミングの概要についての資料を 1 つ以上お読みになっていることを前提とします。今回説明するのは、まったく新しいことではなく、Stack Overflow、MSDN フォーラム、async/await に関するよく寄せられる質問などのオンライン ソースでも同様のアドバイスが見つかります。今回行うのは、利用可能なドキュメントの山に埋もれてしまいがちないくつかのベスト プラクティスを取り上げることだけです。

今回取り上げるベスト プラクティスは、実際の規則というよりは「指針」と呼ぶ方がふさわしいかもしれません。それぞれの指針には例外があります。そのため、それぞれの指針の背景にある考え方を説明し、何に適用でき、何に適用できないのかがわかるようにします。今回の指針を図 1 にまとめ、順番に説明していきます。

図 1 非同期プログラミングの指針のまとめ

名前 説明 例外
async void を避ける async void メソッドよりも async Task メソッドを利用する イベント ハンドラー
すべて非同期にする ブロッキングと非同期コードを混在しない コンソール Main メソッド
コンテキストを構成する 可能なときには ConfigureAwait(false) を使用する コンテキストを必要とするメソッド

async void を避ける

使用できる戻り値の型には、Task、Task<T>、および void の 3 つがありますが、async メソッドで自然な戻り値の型は Task と Task<T> だけです。同期コードから非同期コードに変換する際、型 T を返すメソッドはすべて Task<T> を返す async メソッドになり、void を返すメソッドはすべて Task を返す async メソッドになります。次のコード スニペットは、void を返す同期メソッドとそれに相当する非同期メソッドを示しています。

void MyMethod()
{
  // Do synchronous work.
  Thread.Sleep(1000);
}
async Task MyMethodAsync()
{
  // Do asynchronous work.
  await Task.Delay(1000);
}

void を返す async メソッドには、非同期イベント ハンドラーを可能にするという明確な目的があります。なんらかの実型を返すイベント ハンドラーを用意できますが、言語とはうまく連携しません。型を返すイベント ハンドラーを呼び出すことは非常に扱いにくく、実際に何かを返すイベント ハンドラーの記法はあまり意味をなしません。イベント ハンドラーは本来 void を返すため、async メソッドから void を返して、非同期イベント ハンドラーを用意できるようにします。ただし、async void メソッドの一部のセマンティクスは、async Task メソッドまたは async Task<T> メソッドのセマンティクスとはやや異なります。

async void メソッドでは、エラー処理のセマンティクスが異なります。async Task メソッドまたは async Task<T> メソッドから例外がスローされると、その例外は Task オブジェクトでキャプチャされ、そこで処理されます。async void メソッドでは Task オブジェクトがないため、async void メソッドからスローされた例外はすべて、async void メソッドが開始されたときにアクティブだった SynchronizationContext で直接発生します。図 2 は、async void メソッドからスローされた例外は本質的にはキャッチできないことを示しています。

図 2 Catch ではキャッチできない async void メソッドの例外

private async void ThrowExceptionAsync()
{
  throw new InvalidOperationException();
}
public void AsyncVoidExceptions_CannotBeCaughtByCatch()
{
  try
  {
    ThrowExceptionAsync();
  }
  catch (Exception)
  {
    // The exception is never caught here!
    throw;
  }
}

このような例外は、AppDomain.UnhandledException、またはすべてをキャッチする GUI/ASP.NET アプリケーション用の同様のイベントを使用して監視できますが、通常の例外処理でこれらのイベントを使うと管理が困難になります。

async void メソッドでは、構成のセマンティクスも異なります。Task または Task<T> を返す async メソッドは、await、Task.WhenAny、Task.WhenAll などを使って簡単に構成できます。void を返す async メソッドは、呼び出し側のコードに完了を通知する簡単な方法がありません。複数の async void メソッドを開始するのは簡単ですが、それぞれがいつ完了したかを判断するのはそう容易なことではありません。async void メソッドは、開始時と終了時に SynchronizationContext に通知しますが、通常のアプリケーション コードではカスタムの SynchronizationContext を作成するのは複雑なソリューションです。

async void メソッドはテストが困難です。エラー処理と構成に違いがあるため、async void メソッドを呼び出す単体テストを作成するのは難しくなります。MSTest の非同期テスト サポートは、Task または Task<T> を返す async メソッド向けにしか動作しません。すべての async void メソッドが完了した時点を検出し、すべての例外を収集する SynchronizationContext をインストールできますが、単に async void メソッドが Task を返すようにする方がずっと簡単です。

async Task メソッドと比べて、async void メソッドには複数のデメリットがあるのは明らかですが、非同期イベント ハンドラーという特定の場合には非常に役立ちます。セマンティクスの違いは、非同期イベント ハンドラーには意味があります。これらは SynchronizationContext で直接例外を発生させるため、同期イベント ハンドラーの動作に似ています。同期イベント ハンドラーは通常プライベートなので、構成したり直接テストすることはできません。ここでは、実際のロジックを含む async Task メソッドを待機するようにするなど、非同期イベント ハンドラーのコードを最小限にする方法を採用します。次のコードは、この方法を示しており、テスト可能性を犠牲にすることなくイベント ハンドラーのために async void メソッドを使います。

private async void button1_Click(object sender, EventArgs e)
{
  await Button1ClickAsync();
}
public async Task Button1ClickAsync()
{
  // Do asynchronous work.
  await Task.Delay(1000);
}

async void メソッドは、呼び出し側が非同期であることを想定していない場合、大惨事につながることがあります。戻り値の型が Task のとき、呼び出し側はその後の操作で処理することを認識しています。戻り値の型が void のとき、呼び出し側は処理が戻ったときにメソッドが完了したと想定します。この問題は、多くの予想外の方法で生じる場合があります。インターフェイス (または基本クラス) で void を返すメソッドの async 実装 (またはオーバーライド) を提供するのは、多くの場合適切ではありません。イベントによっては、処理が戻ったときにハンドラーが完了したと想定されるイベントもあります。非同期ラムダを Action パラメーターを受け取るメソッドに渡す際に、小さな問題が生じる場合があります。この場合、非同期ラムダが void を返すため、async void メソッドのすべての問題が引き継がれます。一般的な規則として、非同期ラムダは Task (Func<Task> など) を返すデリゲート型に変換される場合にのみ使用します。

この最初の指針をまとめると、async void よりも async Task を使用することをお勧めします。async Task メソッドは、エラー処理を簡略化し、構成の可能性とテストの可能性を保証します。この指針の例外は非同期イベント ハンドラーで、この場合は必ず void を返します。この例外には、イベント ハンドラーとは呼ばれていなくても、論理的にイベント ハンドラーの役割を持つメソッドも含まれます (ICommand.Execute の実装など)。

すべて非同期にする

非同期コードと聞くと、次のような話を思い出します。ある男が、世界は宙に浮かんでいるのだと言うと、おばあさんは世界は巨大な亀の背中に乗っているのだと反論しました。男が、それではその亀は何の上に乗っているのかとたずねると、おばあさんはこう答えました。「良い質問ですね。亀の下には無限に亀が重なっているのです」。同期コードを非同期コードに変換する際、上から下に (または下から上に) 非同期コードが他の非同期コードを呼び出す、または呼び出される場合に最もうまく機能することがわかります。他の人も、非同期プログラミングの動作が広まることに気付いており、"伝染" と呼んだり、ゾンビ ウイルスと比較したりしてきました。亀であろうとゾンビであろうと、非同期コードは周囲のコードも非同期にする傾向があることは間違いありません。この動作は、新しい async/await キーワードだけでなく、非同期プログラミングのすべての型に当てはまります。

「すべて非同期にする」とは、結果をよく考えずに同期コードと非同期コードを混在させるべきではないことを表しています。特に、Task.Wait または Task.Result を呼び出して非同期コードでブロックするのは通常適切な考え方ではありません。非同期プログラミングを試しているプログラマーが特に陥りやすい問題で、アプリケーションのごく一部だけを非同期に変換し、それを同期 API でラップし、アプリケーションの残りの部分が変更の影響を受けないようにしているような場合です。残念ながら、これらはデッドロックの問題を生じます。MSDN フォーラム、Stack Overflow、および電子メールで数多くの非同期に関する質問に答えてきましたが、非同期を新しく始めた開発者が基本を学習した後に最もよくするのは、「部分的に非同期コードにするとデッドロックが生じるのはなぜですか」という質問です。

図 3 に示すのは、あるメソッドが非同期メソッドの結果でブロックする簡単な例です。このコードは、コンソール アプリケーションでは適切に機能しますが、GUI または ASP.NET のコンテキストで呼び出されるとデッドロックが生じます。この動作は、デバッガーでステップ実行することは、完了することがない await であることを意味すると考えると、特に混乱を招く可能性があります。デッドロックの実際の原因は、Task.Wait が呼び出されたときのコール スタックまでさかのぼります。

図 3 非同期コードをブロックする際によくあるデッドロックの問題

public static class DeadlockDemo
{
  private static async Task DelayAsync()
  {
    await Task.Delay(1000);
  }
  // This method causes a deadlock when called in a GUI or ASP.NET context.
  public static void Test()
  {
    // Start the delay.
    var delayTask = DelayAsync();
    // Wait for the delay to complete.
    delayTask.Wait();
  }
}

このデッドロックの根本的な原因は、await がコンテキストを処理する方法にあります。既定では、未完了の Task を待機するときは、現在の "コンテキスト" がキャプチャされ、Task が完了するときのメソッドの再開に使用されます。この "コンテキスト" は現在の SynchronizationContext で、Null の場合は現在の TaskScheduler になります。GUI アプリケーションと ASP.NET アプリケーションには、一度に実行するコードを 1 つのチャンクに限定する SynchronizationContext があります。await が完了するときは、キャプチャしたコンテキスト内で async メソッドの残りを実行しようとします。しかし、このコンテキストは既にその内部にスレッドを持っており、これは asyncメソッドが完了するのを (同期して) 待機します。それらは、それぞれもう一方を待機し、デッドロックを引き起こします。

コンソール アプリケーションはこのデッドロックを引き起こしません。コンソール アプリケーションでは、一度に 1 つのチャンクに制限する SynchronizationContext ではなく、スレッド プールを備えた SynchronizationContext を使用するため、await が完了するとき、スレッド プールのスレッドで async メソッドの残り処理のスケジュールが設定されます。このメソッドは完了でき、返されたタスクを完了するため、デッドロックは発生しません。この動作の違いにより、プログラマーがテスト コンソール プログラムを作成し、部分的な非同期コードが想定どおりに動作するのを確認した後、同じコードを GUI アプリケーションまたは ASP.NET アプリケーションに移行するとデッドロックが発生する場合があるため、混乱が生じます。

この問題の最適な解決策は、非同期コードをコードベース全体に自然に拡張できるようにすることです。この解決策に従えば、非同期コードがコードベースのエントリ ポイント (通常はイベント ハンドラーまたはコントローラー操作) に拡張されます。コンソール アプリケーションでは、Main メソッドを非同期にできないため、完全にこの解決策に従うことはできません。Main メソッドを非同期にすると、完了前に処理が戻り、プログラムが終了することになります。図 4 は、この指針の例外を示しています。コンソール アプリケーションの Main メソッドは、非同期メソッドでコードがブロックされる場合がある数少ない状況の 1 つです。

図 4 Task.Wait または Task.Result を呼び出す場合がある Main メソッド

class Program
{
  static void Main()
  {
    MainAsync().Wait();
  }
  static async Task MainAsync()
  {
    try
    {
      // Asynchronous implementation.
      await Task.Delay(1000);
    }
    catch (Exception ex)
    {
      // Handle exceptions.
    }
  }
}

非同期をコードベース全体に拡張できるようにするのが最適な解決策ですが、この解決策では、アプリケーションで非同期コードの真のメリットを得るには多くの初期作業が必要になります。大規模なコードベースを非同期コードに段階的に変換する方法はいくつかありますが、今回の目的を超えています。場合によっては、Task.Wait または Task.Result を使用すると部分的な変換に役立ちますが、デッドロックの問題やエラー処理の問題に注意が必要です。ここでは、まずエラー処理の問題について説明し、デッドロックの問題を回避する方法については後半で示します。

すべての Task は、例外のリストを格納します。Task を待機する際、最初の例外が再度スローされ、具体的な例外の型をキャッチできます (InvalidOperationException など)。ただし、Task.Wait や Task.Result を使って Task で同期してブロックする場合、例外がすべて AggregateException にラップされてスローされます。図 4 をもう 1 度参照してください。MainAsync の try/catch は、具体的な例外型をキャッチしますが、try/catch を Main に配置すると、常に AggregateException をキャッチします。AggregateException がない場合、エラー処理は扱いが非常に簡単になるので、"グローバル" な try/catch を MainAsync に配置します。

ここまでは、起こり得るデッドロックと、より複雑なエラー処理という非同期コードのブロックに関する 2 つの問題を示してきました。非同期メソッド内でブロック コードを使用することに関わる問題もあります。次のシンプルな例を考えてみましょう。

public static class NotFullyAsynchronousDemo
{
  // This method synchronously blocks a thread.
  public static async Task TestNotFullyAsync()
  {
    await Task.Yield();
    Thread.Sleep(5000);
  }
}

このメソッドは、完全に非同期ではありません。これはすぐに処理を明け渡し、未完了のタスクを返しますが、再開時にはどのスレッドが実行されていても、同調してブロックされます。このメソッドが GUI コンテキストから呼び出されると、GUI スレッドがブロックされ、ASP.NET 要求コンテキストから呼び出されると、現在の ASP.NET 要求スレッドがブロックされます。非同期コードは、同調してブロックされなければ最適に機能します。図 5 は、同期操作を非同期に置き換える場合のチート シートです。

図 5 物事を "非同期に行う方法"

目的 元のメソッド 代わりに使用するメソッド
バックグラウンド タスクの結果を取得する Task.Wait または Task.Result await
任意のタスクの完了を待機する Task.WaitAny await Task.WhenAny
複数タスクの結果を取得する Task.WaitAll await Task.WhenAll
一定時間待機する Thread.Sleep await Task.Delay

この 2 つ目の指針をまとめると、async とブロッキング コードを混在させるべきではありません。async とブロッキング コードを混在させるとデッドロック、複雑なエラー処理、およびコンテキスト スレッドの予期しないブロックが生じる場合があります。この指針の例外は、コンソール アプリケーションの Main メソッド、または高度なユーザーの場合は、部分的に非同期のコードベースの管理です。

コンテキストを構成する

未完了の Task を待機しているときに、既定で "コンテキスト" がキャプチャされ、キャプチャされたコンテキストが async メソッドの再開に使用されるしくみについて既に簡単に説明しました。図 3 の例では、コンテキストでの再開が同期ブロックによって妨げられ、デッドロックが起こることを示しました。このコンテキストの動作は、パフォーマンスに関する問題も引き起こすことがあります。非同期 GUI アプリケーションの規模が大きくなると、コンテキストとして GUI スレッドを使用する async メソッドの小さな部品が数多く散見されるようになります。これは、1 つ 1 つではさほど影響はなくても、"たくさんになると" 応答に遅れが生じることがあります。

この状況を緩和するには、できる限り ConfigureAwait の結果を待機します。次のコード スニペットは、既定のコンテキストの動作と ConfigureAwait の使い方を示しています。

async Task MyMethodAsync()
{
  // Code here runs in the original context.
  await Task.Delay(1000);
  // Code here runs in the original context.
  await Task.Delay(1000).ConfigureAwait(
    continueOnCapturedContext: false);
  // Code here runs without the original
  // context (in this case, on the thread pool).
}

ConfigureAwait を使用すると、少量の並列処理が可能になります。一部の非同期コードは、ちょっとした作業を絶え間なくさせるのではなく、GUI スレッドと並列で実行できます。

パフォーマンスに加えて、ConfigureAwait にはデッドロックを回避できるというもう 1 つの重要な側面があります。図 3 を再度考えてみましょう。DelayAsync のコード行に "ConfigureAwait(false)" を追加すると、デッドロックが回避されます。この場合、await が完了するとき、スレッド プールのコンテキスト内で async メソッドの残り処理の実行が試みられます。このメソッドは完了でき、返されたタスクを完了するため、デッドロックは発生しません。この手法は、アプリケーションを同期から非同期に段階的に変換する必要がある場合に特に役立ちます。

メソッド内のどこかの時点で ConfigureAwait を使用できる場合、その時点以降のメソッドではすべて await の代わりにこちらを使用することをお勧めします。コンテキストがキャプチャされるのは未完了の Task を待機している場合に限られ、Task が既に完了している場合はコンテキストはキャプチャされません。一部のタスクは、ハードウェアやネットワーク ソリューションが異なる場合に想定よりも早く完了する場合があるため、待機前に完了して返されたタスクを処理する必要があります。図 6 は、修正した例を示しています。

図 6 待機前に完了して返されたタスクを処理する

async Task MyMethodAsync()
{
  // Code here runs in the original context.
  await Task.FromResult(1);
  // Code here runs in the original context.
  await Task.FromResult(1).ConfigureAwait(continueOnCapturedContext: false);
  // Code here runs in the original context.
  var random = new Random();
  int delay = random.Next(2); // Delay is either 0 or 1
  await Task.Delay(delay).ConfigureAwait(continueOnCapturedContext: false);
  // Code here might or might not run in the original context.
  // The same is true when you await any Task
  // that might complete very quickly.
}

コンテキストが必要なメソッドで await の後にコードがある場合、ConfigureAwait を使用すべきではありません。GUI アプリの場合は、GUI 要素を実行するすべてのコードがこれに該当し、データバインド プロパティを記述するか、Dispatcher/CoreDispatcher などの GUI 固有の型を利用します。ASP.NET アプリの場合は、コントローラー アクションの return ステートメントなど、HttpContext.Current を使用するか、ASP.NET 応答を構築するすべてのコードがこれに該当します。図 7 は、GUI アプリでよく使われる 1 つのパターンを示しています。このパターンでは、メソッドの最初で非同期イベント ハンドラーが制御を無効にし、いくつかの待機メソッドを実行してから、ハンドラーの終了時に制御を再度有効にします。この場合イベント ハンドラーは、制御を再度有効にする必要があることから、コンテキストを保持する必要があります。

図 7 非同期イベント ハンドラーを無効にして制御を再度有効にする

private async void button1_Click(object sender, EventArgs e)
{
  button1.Enabled = false;
  try
  {
    // Can't use ConfigureAwait here ...
    await Task.Delay(1000);
  }
  finally
  {
    // Because we need the context here.
    button1.Enabled = true;
  }
}

各 async メソッドには固有のコンテキストがあり、async メソッドが別の async メソッドを呼び出す場合、それぞれのコンテキストは無関係になります。図 7 に少し修正を加えたものを図 8 に示します。

図 8 固有のコンテキストを持つ各 async メソッド

private async Task HandleClickAsync()
{
  // Can use ConfigureAwait here.
  await Task.Delay(1000).ConfigureAwait(continueOnCapturedContext: false);
}
private async void button1_Click(object sender, EventArgs e)
{
  button1.Enabled = false;
  try
  {
    // Can't use ConfigureAwait here.
    await HandleClickAsync();
  }
  finally
  {
    // We are back on the original context for this method.
    button1.Enabled = true;
  }
}

コンテキストに依存しないコードは、再利用しやすくなります。コンテキストに依存するコードとコンテキストに依存しないコードの間でコードにバリアの作成を試みるには、コンテキストに依存するコードを最小限に抑えます。図 8 では、イベント ハンドラーのすべてのコア ロジックを、テスト可能でコンテキストに依存しない async Task メソッドに配置し、コンテキストに依存するイベント ハンドラーのコードを最小限に抑えることをお勧めします。ASP.NET アプリケーションを作成している場合でも、デスクトップ アプリケーションと共有する可能性があるコア ライブラリがあるときは、ライブラリ コードで ConfigureAwait を使用することを検討します。

3 つ目の指針をまとめると、可能な場合は常に ConfigureAwait を使用すべきであるということになります。コンテキストに依存しないコードは、GUI アプリケーションのパフォーマンスを向上し、部分的に非同期のコードベースに取り組む際のデッドロックを回避するのに役立ちます。この指針の例外は、コンテキストが必要なメソッドです。

自分のツールを理解する

async と await については学習することがたくさんあり、やや間違った方向に進むことも少なくありません。図 9 に示すのは、よくある問題の解決策の簡単な参考例です。

図 9 非同期でよくある問題の解決策

問題 解決策
タスクを作成してコードを実行する Task.Run または TaskFactory.StartNew (Task のコンストラクターまたは Task.Start "以外")
操作またはイベントのタスク ラッパーを作成する TaskFactory.FromAsync または TaskCompletionSource<T>
キャンセルをサポートする CancellationTokenSource および CancellationToken
進行状況を報告する IProgress<T> および Progress<T>
データ ストリームを処理する TPL Dataflow または Reactive Extensions
共有リソースへのアクセスを同期する SemaphoreSlim
リソースを非同期に初期化する AsyncLazy<T>
プロデューサー/コンシューマー構造を async 対応にする TPL Dataflow または AsyncCollection<T>

最初の問題は、タスクの作成です。async メソッドはタスクを作成でき、これが最も簡単な方法であることは明らかです。スレッド プールでコードを実行する必要がある場合は Task.Run を使用し、既存の非同期操作または非同期イベント用にタスク ラッパーを作成する場合は TaskCompletionSource<T> を使用します。次の問題は、キャンセルの処理方法と進捗状況の報告方法です。基本クラス ライブラリ (BCL) には、この問題を解決するための CancellationTokenSource/CancellationToken と IProgress<T>/Progress<T> という型があります。非同期コードでは、タスクベースの非同期パターン (TAP: Task-based Asynchronous Pattern) を使用します (msdn.microsoft.com/library/hh873175、英語)。このパターンでは、タスクの作成、キャンセル、進捗状況報告について詳しく示されています。

また、非同期データ ストリームの処理方法も問題になります。タスクは優れていますが、1 つのオブジェクトしか返さず、1 回しか完了しません。非同期ストリームの場合、TPL Dataflow または Reactive Extensions (Rx) のどちらかを使用できます。TPL Dataflow は、アクターのような感じを持つ "メッシュ" を作成します。Rx の方が強力で効果的ですが、学習が複雑になります。TPL Dataflow と Rx にはどちらも async 対応のメソッドがあり、非同期コードで適切に機能します。

コードが非同期であれば安全だというわけではありません。依然として共有リソースを保護する必要があり、ロックの内部からは待機できないため複雑になります。以下に、常に同じスレッドで実行する場合でも、2 回実行されたときに共有状態を壊す可能性がある async コードの例を示します。

int value;

Task<int> GetNextValueAsync(int current);

async Task UpdateValueAsync()

{

  value = await GetNextValueAsync(value);

}

問題は、メソッドが await で値を読み取り自身を中断するため、メソッドが再開するときに値は変更されていないと想定することです。この問題を解決するため、SemaphoreSlim クラスは async 対応の WaitAsync オーバーロードを備えるよう強化されました。図 10 は、SemaphoreSlim.WaitAsync の使い方を示しています。

図 10 SemaphoreSlim は非同期の同期を可能にする

SemaphoreSlim mutex = new SemaphoreSlim(1);

int value;

Task<int> GetNextValueAsync(int current);

async Task UpdateValueAsync()

{

  await mutex.WaitAsync().ConfigureAwait(false);

  try

  {

    value = await GetNextValueAsync(value);

  }

  finally

  {

    mutex.Release();

  }

}

非同期コードは、多くの場合、その後キャッシュおよび共有されるリソースの初期化に使用されます。このための組み込み型はありませんが、Stephen Toub は Task<T> と Lazy<T> のマージのように動作する AsyncLazy<T> を開発しました。オリジナルの型は彼のブログ (bit.ly/dEN178、英語) で説明されていて、更新したバージョンは私の AsyncEx ライブラリ (nitoasyncex.codeplex.com、英語) から入手できます。

最後に、なんらかの async 対応のデータ構造が必要になる場合があります。TPL Dataflow は、async 対応のプロデューサー/コンシューマー キューのように動作する BufferBlock<T> を提供します。また、AsyncEx が AsyncCollection<T> を提供します。これは、BlockingCollection<T> の非同期バージョンです。

今回で示した指針と助言が役に立つことを祈っています。非同期は、真に優れた言語機能で、使い始めるには今がベストです。

Stephen Cleary は、ミシガン北部在住の夫、父親兼プログラマーです。彼は、マルチスレッドと非同期プログラミングに 16 年間取り組み、最初の CTP から Microsoft .NET Framework の非同期サポートを使ってきました。彼のホーム ページとブログは、stephencleary.com (英語) から利用できます。

この記事のレビューに協力してくれた技術スタッフの Stephen Toub に心より感謝いたします。
Stephen Toub はマイクロソフトの Visual Studio チームで働いています。彼の専門は並列処理と非同期処理に関連する分野です。