次の方法で共有


非同期プログラミング

await による一時停止と再生

Mads Torgersen

コード サンプルのダウンロード

次期バージョンの Visual Basic と C# における非同期メソッドは、非同期プログラミングからコールバックを取り除く優れた手段です。今回の記事では、await という新しいキーワードの実際の動作について、概念のレベルからその課題まで詳しく説明します。

シーケンシャル構成

Visual Basic と C# は命令型プログラミング言語であり、これは Visual Basic と C# の特長でもあります。つまり、これらの言語では、順次実行される「順番に並べられた個別の手順」としてプログラミング ロジックを表現します。ステートメント レベルのほとんどの言語構成要素は制御構造です。この制御構造を使用して、特定のコード本文での個別手順の実行順序をさまざまな方法で指定できます。

  • if や switch などの条件付きステートメントでは、現在状態に基づいて、次に実行する処理を選択できます。
  • for、foreach、while などのループ ステートメントでは、一連の手順を繰り返し実行できます。
  • continue、throw、goto などのステートメントでは、プログラムの局所的ではない別の場所に制御を移すことができます。

制御構造を使用してロジックを組み立てると "シーケンシャル構成" になります。このシーケンシャル構成こそ、命令型言語に不可欠なものです。そのため、選択する制御構造がたくさん用意されています。非常に便利で優れた構造のロジックには、シーケンシャル構成がお勧めです。

連続実行

現在のバージョンの Visual Basic と C# を含めたほとんどの命令型言語では、メソッド (関数、プロシージャなどとも呼びます) が "連続的" に実行されます。"連続的" とは、"制御のスレッド" で特定のメソッドの実行を開始すると、メソッドの実行が終わるまでずっとこの処理にスレッドが占有されることを意味します。もちろん、このスレッドでは、コード本文から呼び出したメソッドのステートメントを実行することもありますが、それはメソッド実行の一環にすぎません。メソッドに含まれない処理を実行するよう切り替えることはありません。

このような連続性が問題になることがあります。メソッドでは処理を進めるうえで何もできず、何か (ダウンロード、ファイル アクセス、別のスレッドで行われるコンピューター処理、特定の時点の到来など) が起こるのを待機するしかないことがあります。このような場合、スレッドは何も処理しないまま完全に占有されます。一般に、この状況をスレッドが "ブロックされている" 状態と呼び、原因となっているメソッドがスレッドを "ブロックしている" と表現します。

深刻なブロックを引き起こすメソッドの例を次に示します。

static byte[] TryFetch(string url)
{
  var client = new WebClient();
  try
  {
    return client.DownloadData(url);
  }
  catch (WebException) { }
  return null;
}

このメソッドを実行するスレッドは、client.DownloadData の呼び出し中の大半は、実際には何もせず待機しているだけです。

スレッドが貴重なときは、この状況は問題です。しかも、スレッドはたいてい貴重です。標準の中間層では、要求を処理するたびに、バックエンドや他のサービスとの対話が必要になります。各要求がその要求専用のスレッドで処理され、このようなスレッドの大半が中間結果を待機してブロックされる場合は、中間層でのスレッド数が膨大になり、すぐにパフォーマンスのボトルネックになります。

最も貴重なスレッドは、おそらく UI スレッドです。UI スレッドは 1 つしかありません。ほとんどすべての UI フレームワークはシングル スレッドで、UI に関連するあらゆる処理 (イベント、更新、ユーザーの UI 操作ロジックなど) を同一の専用スレッドで実行する必要があります。いずれかのアクティビティ (URL からのダウンロードを選択するイベント ハンドラーなど) で待機が始まると、UI スレッドがまったく何も実行しないまま占領され、UI 全体の処理が進まなくなります。

ここで必要なのは、複数のシーケンシャル アクティビティがスレッドを共有する方法です。そのためには、シーケンシャル アクティビティをときどき "休止" する必要があります。"休止" とは、同じスレッドで他のアクティビティを処理できるように、アクティビティの実行に穴を開けることです。つまり、アクティビティをときどき "不連続" にする必要があります。何も実行していないシーケンシャル アクティビティは、このように休止すると特に効果的です。ここで活躍するのが、非同期プログラミングです。

非同期プログラミング

メソッドは常に連続的なので、不連続のアクティビティ (ダウンロード前後のアクティビティなど) を複数のメソッドに振り分ける必要があります。メソッドの実行の途中に穴を開けるには、連続処理を行う複数の部分にメソッドを分割します。API には実行時間の長いメソッドの非同期 (ブロックしない) バージョンが用意されています。この非同期バージョンは、操作 (ダウンロードの開始など) を開始し、操作完了時に実行するコールバックを渡して格納後、すぐに呼び出し元に復帰します。ただし、呼び出し元にコールバックを渡すには、"操作後" のアクティビティを個別のメソッドに作り変える必要があります。

そのためには、先ほどの TryFetch メソッドを次のように変更します。

static void TryFetchAsync(string url, Action<byte[], Exception> callback)
{
  var client = new WebClient();
  client.DownloadDataCompleted += (_, args) =>
  {
    if (args.Error == null) callback(args.Result, null);
    else if (args.Error is WebException) callback(null, null);
    else callback(null, args.Error);
  };
  client.DownloadDataAsync(new Uri(url));
}

ここには、コールバックを渡す異なる方法がいくつか使われています。DownloadDataAsync メソッドは DownloadDataCompleted イベントにイベント ハンドラーが登録されていると想定しており、これがメソッドの "操作後" の部分を渡す方法に当たります。TryFetchAsync メソッド自体でも、呼び出し元のコールバックを処理する必要があります。イベントの処理全体を自分で設定する代わりに、コールバックをパラメーターとして受け取るだけの単純な方法を使用できます。すばらしいことに、イベント ハンドラーにラムダ式を使用できるため、callback パラメーターを直接取得して使用できます。名前付きメソッドを使用するとしたら、イベント ハンドラーへのコールバック デリゲートを取得する方法を編み出す必要があるでしょう。ちょっと立ち止まって、ラムダを使用せずにこのコードを作成する方法を考えてみてください。

ただし、ここで最も注目すべき点は、制御フローの変わり方です。フローの表現に言語の制御構造を使用せず、この制御構造のエミュレーションを行います。

  • コールバックを呼び出すことによって、return ステートメントのエミュレーションを行う。
  • コールバックを呼び出すことによって、例外の暗黙の伝播のエミュレーションを行う。
  • 型チェックにより、例外処理のエミュレーションを行う。

もちろん、これは非常に単純な例です。目的の制御構造が複雑になると、エミュレーションもさらに複雑になります。

まとめると、処理を不連続にしたことで、ダウンロードの待機中に実行スレッドで別の処理を実行できるようになります。しかし、制御構造を使用したフロー表現の容易さが失われます。構造化された命令型言語としてのメリットを放棄したのです。

非同期メソッド

問題をこのように考えると、次期バージョンの Visual Basic と C# での非同期メソッドの効果がはっきりします。非同期メソッドを使用すると、不連続なシーケンシャル コードを表現できます。

この新しい構文を使用した TryFetch メソッドの非同期バージョンを見てみましょう。

static async Task<byte[]> TryFetchAsync(string url)
{
  var client = new WebClient();
  try
  {
    return await client.DownloadDataTaskAsync(url);
  }
  catch (WebException) { }
  return null;
}

非同期メソッドを使用すると、コードの途中で、インラインで休止できます。使い慣れた制御構造を使用してシーケンシャル構成を表現できるだけでなく、await 式を使用して実行の途中に穴を開けることができます。実行スレッドのこの穴で他の処理を自由に実行できます。

この動作を簡単に理解するには、非同期メソッドに "一時停止" ボタンと "再生" ボタンがあるところを想像してみてください。実行スレッドが await 式に到達すると、一時停止ボタンが押され、メソッドの実行が中断されます。待機対象のタスクが完了すると、再生ボタンが押され、メソッドの実行が再開されます。

コンパイラの書き直し

複雑なものが単純に見える場合、通常、内部では興味深い処理が行われています。これは非同期メソッドについても間違いなく当てはまります。単純に見せることで、非同期コードの作成と読み取りの両方が非常に容易になる優れた抽象化が実現されています。内部のしくみは、必ずしも理解することはありませんが、しくみを理解していれば、非同期プログラマとしてのスキル向上や非同期機能のフル活用にきっと役立ちます。また、この記事の読者なら、しくみに興味をお持ちだと思います。では、非同期メソッド (およびその await 式) で実際に行われている処理について説明しましょう。

Visual Basic または C# のコンパイラは、非同期メソッドを見つけると、コンパイル時にそのメソッドを細分化します。メソッドの不連続性は、基盤となるランタイムでは直接サポートされないため、コンパイラがエミュレーションを行う必要があります。したがって、開発者がメソッドを細分化しない代わりに、コンパイラが細分化することになります。ただし、この細分化の方法は、手動で行う場合と大きく異なります。

コンパイラは、非同期メソッドを "ステート マシン" に変換します。このステート マシンは、実行の進捗状況とローカルの状態を追跡します。状態には、"実行中" と "中断" の 2 種類があります。実行中に await に到達すると、一時停止ボタンが押され、実行が中断されます。中断中に再生ボタンが押されると、再び実行中の状態に戻ります。

await 式は、待機対象のタスクが完了すると再生ボタンが押されたかのような設定を行います。この処理内容を詳しく説明する前にステート マシン自体について説明し、これらの一時停止ボタンと再生ボタンの実態について説明しましょう。

タスク ビルダー

非同期メソッドは、Task を生成します。具体的には、非同期メソッドは、System.Threading.Tasks に含まれる Task 型または Task<T> 型のいずれかのインスタンスを返し、そのインスタンスを自動生成します。ユーザー コードでインスタンスを生成する必要 (および機能) はありません (これは少し不正確です。非同期メソッドは void を返すことができます。ただし、今のところはこの動作を無視することにします)。

コンパイラを中心に考えると、Task の生成は簡単です。この作業では、フレームワークから提供される Task builder (タスク ビルダー) の概念を利用しており、このタスク ビルダーは System.Runtime.CompilerServices に含まれています (通常は人間が直接使用することを想定していないため)。たとえば、次のような型があります。

public class AsyncTaskMethodBuilder<TResult>
{
  public Task<TResult> Task { get; }
  public void SetResult(TResult result);
  public void SetException(Exception exception);
}

このビルダーにより、コンパイラは Task を取得でき、結果または例外を添えて Task を完了できます。図 1 は、TryFetchAsync メソッドの場合のこのメカニズムを示しています。

図 1 Task のビルド

static Task<byte[]> TryFetchAsync(string url)
{
  var __builder = new AsyncTaskMethodBuilder<byte[]>();
  ...
  Action __moveNext = delegate
  {
    try
    {
      ...
      return;
      ...
      __builder.SetResult(…);
      ...
    }
    catch (Exception exception)
    {
      __builder.SetException(exception);
    }
  };
  __moveNext();
  return __builder.Task;
}

次の点に注目してください。

  • まず、ビルダーが作成されます。
  • 次に、__moveNext デリゲートが作成されます。このデリゲートが、再生ボタンです。ここではこのデリゲートを再開デリゲートと呼び、次の項目を含みます。
    • 非同期メソッドの本来のコード (ただし、上記では省略)。
    • 一時停止ボタンを押したことを表す return ステートメント。
    • 成功した結果を添えてビルダーを完了する呼び出し。これは、本来のコードの return ステートメントに相当します。
    • エスケープされた例外を添えてビルダーを完了する、ラップした try/catch。
  • ここで、再生ボタンが押され、再開デリゲートが呼び出されます。このデリゲートは、一時停止ボタンが押されるまで実行します。
  • 最後に、Task を呼び出し元に返します。

タスク ビルダーは、コンパイラが使用することだけを目的とした特殊なヘルパーの種類です。しかし、その動作は、Task Parallel Library (TPL: タスク並列ライブラリ) の TaskCompletionSource 型を直接使用する場合の動作と、たいして違いません。

ここまでは、返す Task と、実行を再開する際にメソッドで呼び出す再生ボタン (再開デリゲート) を作成しました。実行の再開方法と、そのために await 式を設定する方法の説明がまだ残っていますが、全体を 1 つにまとめる前に、Task の使用方法について説明しましょう。

待機可能な型と待機側

ここまで説明したように、Task を待機できます。ただし、Visual Basic と C# では、他の型も "待機可能" であれば、つまり await 式のコンパイル対象にできる一定形式の型であれば、待機できます。型が待機可能であるためには、("待機側" を返す) GetAwaiter メソッドがその型に存在している必要があります。たとえば、Task<TResult> 型には、次の型を返す GetAwaiter メソッドがあります。

public struct TaskAwaiter<TResult>
{
  public bool IsCompleted { get; }
  public void OnCompleted(Action continuation);
  public TResult GetResult();
}

待機側のメンバーにより、待機可能な型が既に完了しているかどうかをコンパイラが確認して、完了していない場合はその型のコールバックを登録でき、完了している場合は結果 (または例外) を取得できます。

これで、待機可能な型に基づいて一時停止および再開するために await が実行する動作を説明する準備が整いました。たとえば、TryFetchAsync メソッドの例に含まれる await からは、次のコードが生成されます。

 

__awaiter1 = client.DownloadDataTaskAsync(url).GetAwaiter();
  if (!__awaiter1.IsCompleted) {
    ... // Prepare for resumption at Resume1
    __awaiter1.OnCompleted(__moveNext);
    return; // Hit the "pause" button
  }
Resume1:
  ... __awaiter1.GetResult()) ...

ここでは、次の点に注目します。

  • DownloadDataTaskAsync から返される Task 用に、待機側を取得します。
  • 待機側が完了していない場合、再生ボタン (再開デリゲート) をコールバックとして待機側に渡します。
  • 待機側が (Resume1 で) 実行を再開すると、結果を取得し、以降のコードで使用します。

明らかに、たいていの場合は、Task または Task<T> が待機可能な型です。実際、これらの型は、このような用途に合わせて積極的に最適化されています (Microsoft .NET Framework 4 で既に存在しています)。しかし、他の待機可能な型を使用する意義も十分にあります。

  • 他のテクノロジへの橋渡し: たとえば F# には、Func<Task<T>> にほぼ相当する Async<T> 型があります。Visual Basic や C# から直接 Async<T> を待機できると、2 つの言語で記述された非同期コードの橋渡しが可能です。同様に F# でも 逆方向の橋渡し機能が公開され、非同期 F# コードで Task 型を直接使用できます。
  • 特殊な意味合いの実装: この実装に関しては、TPL 自体で簡単な例をいくつか追加しています。たとえば、静的な Task.Yield ユーティリティ メソッドから返される待機可能な型は、完了してないことを (IsCompleted を介して) 通知しますが、まるで実際に完了したかのように、OnCompleted メソッドに渡すコールバックのスケジュールをすぐに設定します。これにより、スケジュールを強制的に設定でき、結果を既に使用できる場合にスキップされるコンパイラの最適化を迂回できます。この手法を使用すると、"実行中の" コードに穴を開けることができ、アイドル状態のコードの応答性を向上できます。Task 自体は、完了した項目を表せなくてもその項目が完了していないことは通知できるため、特殊な待機可能な型はこのような目的に使用します。

Task の待機可能な実装について詳しく説明する前に、コンパイラによる非同期メソッドの書き直しの残りを説明し、メソッドの実行状態を追跡するブックキーピング処理を具体的に示します。

ステート マシン

ここまで説明してきたロジック全体を 1 つにまとめるには、Task の生成と使用に関連するステート マシンを構築する必要があります。基本的に、元のメソッドのユーザー ロジックはすべて再開デリゲートに配置されますが、ローカル変数の宣言は退避され、複数の呼び出しの間保持されます。さらに、状態変数を導入して進捗状況を追跡し、再開デリゲートのユーザー ロジックを、状態を確認して対応するラベルに移動する大規模なスイッチでラップします。したがって、再開を呼び出すたびに、最後に中断した場所に正確に移動します。図 2 に、すべての処理を示します。

図 2 ステート マシンの作成

static Task<byte[]> TryFetchAsync(string url)
{
  var __builder = new AsyncTaskMethodBuilder<byte[]>();
  int __state = 0;
  Action __moveNext = null;
  TaskAwaiter<byte[]> __awaiter1;
 
  WebClient client = null;
 
  __moveNext = delegate
  {
    try
    {
      if (__state == 1) goto Resume1;
      client = new WebClient();
      try
      {
        __awaiter1 = client.DownloadDataTaskAsync(url).GetAwaiter();
        if (!__awaiter1.IsCompleted) {
          __state = 1;
          __awaiter1.OnCompleted(__moveNext);
          return;
        }
        Resume1:
        __builder.SetResult(__awaiter1.GetResult());
      }
      catch (WebException) { }
      __builder.SetResult(null);
    }
    catch (Exception exception)
    {
      __builder.SetException(exception);
    }
  };
 
  __moveNext();
  return __builder.Task;
}

とてもわかりにくいコードです。きっと、手作業で "非同期" にしたコードに比べて、なぜこのように冗長になるのか疑問に思われるでしょう。このように冗長になるのには、効率 (一般には、割り当て回数の削減)、汎用性 (Task だけではない、待機可能なユーザー定義型) など、いくつか正当な理由があります。しかし、最大の理由は、ユーザー ロジックを分割する必要がなく、いくつかのジャンプや復帰などでロジックを補強するだけで済むことです。

この例は単純すぎて正当性を十分確認できませんが、メソッドのロジックを書き直して、await と await の間の連続するロジックごとに分割したメソッドと同じ意味合いを持つ個別メソッドにすることは、非常に厄介な作業です。await を入れ子にする制御構造が多くなるほど、作業が厄介になります。continue ステートメントや break ステートメントで単にループに対処するのではなく、try-finally ブロックや goto ステートメントで await を囲んでいる場合、本来のロジックを忠実に再現するよう書き直すことは、不可能でないとしても非常に困難です。

このような困難な作業を試みる代わりに考えられる便利な手段は、ユーザーが記述した本来のコードを別の層の制御構造で覆って、状況に応じて制御構造に (条件付きジャンプを使用して) 入ったり、制御構造から (値を返して) 出たりする方法です。つまり、再生と一時停止です。マイクロソフトでは、同期メソッドと非同期メソッドの同等性を体系的にテストしており、この手法が非常に堅牢なことを確認済みです。同期の意味合いを非同期の体系に持ち込むには、同期の意味合いをもともと記述していたコードを残すのが最適です。

その他の課題

ここまでの説明は少し理想化しています。既に推測されていた方もいらっしゃるでしょうが、書き直しにはもう少しポイントがあります。コンパイラが対処する必要があるその他の課題をいくつか紹介します。

goto ステートメント: 図 2 のように書き直しても、(少なくとも C# の) goto ステートメントは、入れ子構造に埋め込まれたラベルにはジャンプできないため、実際にはコンパイルされません。コンパイラはソース コードではなく中間言語 (IL) を生成するので、この動作自体は問題にならず、入れ子によって面倒が引き起こされることはありません。しかし、IL でも、今回の例のように try ブロックの途中にはジャンプできません。実際には try ブロックの先頭にジャンプし、通常どおりブロックに入ってから、再度切り替えてジャンプします。

finally ブロック: await により、再開デリゲートから値を返すときは、finally ブロックの本文が実行済みだと想定しないことをお勧めします。finally ブロックは、ユーザー コード "本来の" return ステートメントが実行されるまで実行されません。この動作を制御するには、finally ブロックの本文を実行するかどうか通知し確認のためにブロックの本文を補強する、ブール型のフラグを生成します。

評価順序: メソッドや演算子の最初の引数には await 式が必要なく、メソッドや演算子の途中で await 式を使用します。評価順序を保持するには、前にあるすべての引数を await の前に評価する必要があり、await 後に引数を格納して再取得する手法は驚くほど複雑です。

これらの課題に加えて、回避できない制限事項もいくつかあります。たとえば、catch ブロックや finally ブロックの内部には await を配置できません。これは、await 後の正しい例外コンテキストを再構築する適切な方法が確立されていないためです。

タスクの待機側

コンパイラが生成したコードが await 式を実装するために使用する待機側には、再開デリゲート、つまり以降の非同期メソッドのスケジュール設定方法に関してかなりの自由度があります。ただし、待機側を独自に実装する必要が発生するのは、シナリオが非常に高度な場合です。単独でプラグ可能なスケジュール コンテキストという概念が尊重されるため、Task 自体には、そのスケジュール設定方法に関して非常に柔軟性があります。

スケジュール コンテキストは、マイクロソフトが最初から設計目標にしていればもう少し洗練されていたのではないかと思える概念の 1 つです。現時点では、これはいくつかの既存の概念を融合したものです。マイクロソフトは、全体的な統一概念の導入を図ることで、これ以上概念が混乱しないようにすることにしました。まずは概念レベルの考え方を説明し、次に実現方法について説明しましょう。

待機対象タスク用非同期コールバックのスケジュール設定を支える理念とは、特定の値の "場所" については "以前と同じ場所" で実行し続けることです。この "場所" を、スケジュール コンテキストと呼びます。スケジュール コンテキストは、スレッドに関連する概念で、すべてのスレッドに (最大) 1 つあります。スレッドでの実行中にそのスレッドのスケジュール コンテキストを要求でき、スケジュール コンテキストを把握すると、スレッドでの実行内容のスケジュールを設定できます。

タスクの待機中に非同期メソッドで実行する必要がある操作は、次のとおりです。

  • 中断時: メソッドを実行しているスレッドに対してスケジュール コンテキストを要求します。
  • 再開時: 再開デリゲートのスケジュールをスケジュール コンテキストに再設定します。

なぜこれが重要なのでしょう。UI スレッドについて考えてみましょう。UI スレッドには独自のスケジュール コンテキストがあり、メッセージ キューを介してスケジュール コンテキストを UI スレッドに戻すことで、新しい処理のスケジュールを設定します。つまり、UI スレッドで実行中にタスクを待機している場合、タスクの結果の準備が整うと、非同期メソッドの残りの処理が UI スレッドで再度実行されます。そのため、UI スレッドだけで実行できるあらゆる処理 (UI の操作) を await 後も実行でき、コードの途中で不自然な "スレッドのホップ" が発生しません。

他のスケジュール コンテキストはマルチスレッドです。特に、標準スレッド プールは、1 つのスケジュール コンテキストで表されます。新しい処理のスケジュールが設定されると、プールのいずれかのスレッドで処理が実行されます。そのため、スレッド プールで実行を開始する非同期メソッドは、スレッド プール内で実行され続けますが、スレッド間を "ホップ" する可能性があります。

実際には、スケジュール コンテキストに完全に対応する概念はありません。大まかに言えば、スレッドの SynchronizationContext がスレッドのスケジュール コンテキストとして機能します。そのため、スレッドに SynchronizationContext の 1 つ(ユーザーが実装できる既存の概念) があれば、それが使用されます。1 つもない場合は、スレッドの TaskScheduler (TPL で導入された類似概念) が使用されます。スレッドに SynchronizationContext と TaskScheduler のいずれもないときは、標準スレッド プールに再開スケジュールを設定する、既定の TaskScheduler が使用されます。

もちろん、このようなスケジュール設定では、必ずパフォーマンスが低下します。通常、ユーザー シナリオではこのようなパフォーマンスの低下は無視でき、それに見合うメリットが得られます。実際の有効な処理ごとの管理しやすいサイズに UI コードを分割し、待機対象の結果を使用できるようになったらメッセージ ポンプを介して渡しておくことをお勧めします。

しかし、ライブラリ コードの場合は特に、処理のサイズが細かくなりすぎることがあります。次のコードについて考えてみましょう。

async Task<int> GetAreaAsync()
{
  return await GetXAsync() * await GetYAsync();
}

このコードでは、"正しい" スレッドで乗算を行うためだけに、スケジュール コンテキストが 2 回 (各 await の後に) 再設定されます。しかし、乗算を行うスレッドを問題にする人はいないでしょう。この動作はおそらく無駄であり (頻繁に使用する場合)、回避策もあります。基本的には、次のように Task 型以外の待機可能な型で待機対象の Task 型をラップします。ラップしている型では、スケジュールを再設定する動作を無効にでき、タスクが完了した任意のスレッドで再開を実行でき、コンテキスト スイッチとスケジュールの遅延を回避できます。

async Task<int> GetAreaAsync()
{
  return await GetXAsync().ConfigureAwait(continueOnCapturedContext: false)
    * await GetYAsync().ConfigureAwait(continueOnCapturedContext: false);
}

確かに見栄えは悪くなりましたが、スケジュール設定のボトルネックとなるライブラリ コードで使用する場合は効果的な手段です。

非同期化を推進する

今回は、非同期メソッドの基盤について基本的な説明を行いました。覚えておくと特に役に立つと思われる点は、次のとおりです。

  • コンパイラは、制御構造をそのまま維持することで、制御構造の持つ意味を保持します。
  • 非同期メソッドは新しいスレッドのスケジュールを設定せず、既存のスレッドを多重化できます。
  • タスクの待機中、"本来の場所" の意味を合理的に規定する場所に戻ります。

この記事を読みながら既にコードを作成している方がいるかもしれません。その場合、自分という同一スレッドで、複数の制御フロー (記事を読むこととコードを作成すること) を多重化していることになります。非同期メソッドの機能とは、まさにこのようなことを指します。

Mads Torgersen は、マイクロソフトの C# および Visual Basic 言語チームに所属する主席プログラム マネージャーです。

この記事のレビューに協力してくれた技術スタッフの Stephen Toub に心より感謝いたします。