非同期タスク

タスクを使って非同期プログラミングを簡単に

Igor Ostrovsky

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

非同期プログラミングとは、負荷の高い操作がプログラムの他の部分と同時実行されるように実装する技法で、グラフィカルな UI を備えたプログラムでよく使用されます。それは、負荷の高い操作が完了するまで UI が応答を停止することは、基本的に受け入れ難いと考えられるためです。また、非同期操作は、クライアントからの複数の要求を同時に処理する必要があるサーバー アプリケーションにとっても重要です。

非同期操作の代表例には、サーバーに送信した要求に対する応答の待機、ハード ディスクからのデータ読み取り、スペル チェックのような負荷が高いコンピューター処理の実行などがあります。

ここでは、UI を備えたアプリケーションを例に取り上げて考えてみましょう。このようなアプリケーションは、Windows Presentation Foundation (WPF) か Windows フォームを使用して構築され、コードの大半が UI スレッドで実行されます。これは、UI コントロールで発生するイベントへのイベント ハンドラーが UI スレッドで実行されるためです。ユーザーがボタンをクリックすると、UI スレッドでクリック メッセージが取得され、開発者が用意した Click イベント ハンドラーが実行されます。

ここで、アプリケーションからサーバーに要求を送信して応答を待機する処理を Click イベント ハンドラーに実装するとしましょう。

// !!! Bad code !!!
void Button_Click(object sender, RoutedEventArgs e) {
  WebClient client = new WebClient();
  client.DownloadFile("https://www.microsoft.com", "index.html");
}

このコードの大きな問題点は、Web サイトのダウンロードに数秒以上かかることです。つまり、Button_Click 呼び出しから戻るのに数秒かかります。その結果、UI スレッドが数秒ブロックされ、応答を停止します。インターフェイスの応答が停止するとユーザー エクスペリエンスの低下につながり、かなり受け入れ難いものになります。

サーバーが応答するまでの間のアプリケーション UI の応答性を高めるには、ダウンロードを UI スレッドでの同期操作にしないことが重要です。

では、この UI が応答を停止するという問題を解決しましょう。考えられる (しかし最適ではない) 解決策の 1 つは、サーバーと通信を別のスレッドで行い、UI スレッドがブロックされないようにすることです。ここでは、サーバーとの通信にスレッド プールのスレッドを使用します。

// Suboptimal code
void Button_Click(object sender, RoutedEventArgs e) {
  ThreadPool.QueueUserWorkItem(_ => {
    WebClient client = new WebClient();
    client.DownloadFile(
      "https://www.microsoft.com", "index.html");
  });
}

このコード サンプルでは、先ほど示した例にあった問題が解決され、Button_Click イベントによって UI スレッドがブロックされません。ですが、このスレッドベースのソリューションには深刻な問題が 3 つあります。では、これらの問題について詳しく見てみることにしましょう。

問題 1: スレッド プールのスレッドが浪費される

先ほどの例では、スレッド プールにあるスレッドを使用して、サーバーに要求を送信し、サーバーが応答するまで待機しています。

このスレッド プールのスレッドは、サーバーが応答するまでブロックされている状態になります。WebClient.DownloadFile への呼び出しが完了するまで、このスレッドをプールに返すことはできません。スレッド プールのスレッドがブロックされても UI が応答を停止することはないので、UI スレッドがブロックされるよりもずっと優れた方法ですが、スレッド プールにあるスレッドが 1 つ浪費されることになります。

アプリケーションによって、スレッド プールのスレッドがブロックされるのがそれほど頻繁ではなく、長時間でなければ、パフォーマンスの低下は問題にはなりませんが、頻繁にブロックされるようになると、スレッド プールにかかる負荷が原因で応答性が低下します。スレッド プールは、スレッドを追加作成することでこれに対処しようとしますが、パフォーマンス コストは目に見えて大きくなります。

この記事で解説する非同期プログラミングのパターンはどれも、スレッド プールのスレッドが浪費される問題を解決します。

問題 2: 結果を返す

非同期プログラミングにスレッドを使用すると、ヘルパー スレッドで実行される操作から値を返すことがやや複雑になるという、別の難しさが伴います。

最初の例では、DownloadFile メソッドによって、ダウンロードされた Web ページがローカル ファイルに書き込まれるため、DownloadFile メソッドは void 型で戻り値を返しません。そこで、この問題を確認するために、ダウンロードされた Web ページをファイルに書き込む代わりに、受け取った HTML を (HtmlTextBox という) TextBox の Text プロパティに割り当てるバージョンを考えてみましょう。

これを実装する、単純な (そして誤った) 方法は次のとおりです。

// !!! Broken code !!!
void Button_Click(object sender, RoutedEventArgs e) {
  ThreadPool.QueueUserWorkItem(_ => {
    WebClient client = new WebClient();
    string html = client.DownloadString(
      "https://www.microsoft.com", "index.html");
    HtmlTextBox.Text = html;
  }); 
}

問題は、UI コントロール (HtmlTextBox) が、スレッド プールのスレッドから変更されていることにあります。UI を変更できるのは UI スレッドのみなので、エラーが発生します。WPF と Windows フォームの両方に適用されるこの制約には、十分な根拠があります。

この問題を解決するには、次のように、UI スレッドで同期コンテキストを取得して、スレッド プールのスレッドでそのコンテキストにメッセージをポストします。

void Button_Click(object sender, RoutedEventArgs e) {
  SynchronizationContext ctx = SynchronizationContext.Current;
  ThreadPool.QueueUserWorkItem(_ => {
    WebClient client = new WebClient();
    string html = client.DownloadString(
      "https://www.microsoft.com");
    ctx.Post(state => {
      HtmlTextBox.Text = (string)state;
    }, html);
  });
}

ヘルパー スレッドから値を返すことに関する問題は、UI を備えたアプリケーションに限らないということを認識しておくことが重要です。一般に、あるスレッドから別のスレッドに値を返すことは、同期プリミティブを使用しなくてはならない複雑な問題です。

問題 3: 非同期操作を構成する

また、明示的にスレッドを操作すると、非同期操作を構成するのが難しくなります。たとえば、複数の Web ページを並列にダウンロードする場合、同期コードの記述がさらに困難になり、エラーが発生しやすくなります。

このような実装では、実行中の非同期操作の数を数えるカウンターを管理することになります。カウンターは、スレッド セーフな方法 (たとえば Interlocked.Decrement) で変更しなければなりません。このカウンターが 0 になったら、ダウンロードを処理するコードを実行します。このようなことから、コード量が膨大になり、エラーが起こりやすくなります。

言うまでもなく、構成のパターンが複雑になればなるほど、スレッドベースのパターンを使用して適切に実装することがますます難しくなります。

イベントベースのパターン

Microsoft .NET Framework での非同期プログラミングでよく使われるパターンの 1 つが、イベントベースのモデルです。イベント モデルでは、非同期操作を開始するメソッドを公開し、操作の完了時にイベントを発生させます。

イベント パターンは、非同期操作を公開するプログラム記法の 1 つですが、インターフェイス経由でやり取りする場合のように明確な規約ではありません。クラスの実装者が、パターンをどの程度忠実に踏襲するかを決定できます。図 1 に、イベントベースの非同期プログラミング パターンを正しく実装することで公開されるメソッドの一例を示します。

図 1 イベントベースのパターンのメソッド

public class AsyncExample {
  // Synchronous methods.
  public int Method1(string param);
  public void Method2(double param);

  // Asynchronous methods.
  public void Method1Async(string param);
  public void Method1Async(string param, object userState);
  public event Method1CompletedEventHandler Method1Completed;

  public void Method2Async(double param);
  public void Method2Async(double param, object userState);
  public event Method2CompletedEventHandler Method2Completed;

  public void CancelAsync(object userState);

  public bool IsBusy { get; }

  // Class implementation not shown.
  ...
}

WebClient は、イベントベースのパターンを使用して非同期操作を実装する、.NET Framework のクラスの 1 つです。DownloadString メソッドを非同期操作にするために、WebClient では DownloadStringAsync メソッド、CancelAsync メソッド、および DownloadStringCompleted イベントを公開します。次のコードは、非同期方式での実装方法を示すサンプルです。

void Button_Click(object sender, RoutedEventArgs e) {
  WebClient client = new WebClient();
  client.DownloadStringCompleted += eventArgs => {
      HtmlTextBox.Text = eventArgs.Result;
  };
  client.DownloadStringAsync("https://www.microsoft.com");
}

この実装では、スレッドベースのソリューションでは適切に対応されなかった、スレッドが不必要にブロックされるという、問題 1 が解決されます。DownloadStringAsync への呼び出しからはすぐに戻り、UI スレッドも、スレッド プールのスレッドもブロックされません。ダウンロードはバックグラウンドで実行され、完了すると DownloadStringCompleted イベントが適切なスレッドで実行されます。

DownloadStringCompleted イベント ハンドラーは、スレッドベースのソリューションで必要だった SynchronizationContext コードがなくても、適切なスレッドで実行されます。これは、WebClient が自動的に SynchronizationContext を取得して、コンテキストにコールバックをポストしているためです。一般に、イベントベースのパターンを実装するクラスによって、Completed ハンドラーが必ず適切なスレッドで実行されるようになります。

イベントベースの非同期プログラミング パターンは、むやみにスレッドをブロックしないという観点から効率的で、.NET Framework 全体で広く使用されている 2 つのパターンのうちの 1 つです。ただし、イベントベースのパターンには、次のようにいくつかの制限事項があります。

  • パターンは形式ばっておらず、単なるプログラムの表記法にすぎません。クラスは、必ずしもパターンに忠実である必要はありません。
  • 並列起動される非同期操作の処理や、非同期操作の順次処理など、複数の非同期操作を構成する場合、きわめて困難になる可能性があります。
  • 非同期操作が完了しているかどうかを、ポーリングして確認することができません。
  • このような形式を利用する際は、細心の注意が必要です。たとえば、複数の非同期操作を 1 つのインスタンスを使用して処理するのであれば、登録するイベント ハンドラーをコーディングする際に、対象とする 1 つの非同期操作が何回呼び出されても、その 1 つの操作のみを処理するようにしなければなりません。
  • イベント ハンドラーは、常に、非同期操作が起動されたときに取得した SynchronizationContext で呼び出されます。これは、UI スレッドで実行する必要がない場合でも同じなので、その場合はパフォーマンス コストが高くなります。
  • 実装が難しいうえに、複数の種類 (イベント ハンドラーやイベント引数など) を定義する必要があります。

図 2 に、イベントベースの非同期パターンを実装する .NET Framework 4 のクラスの例をいくつか示します。

図 2 .NET クラスでのイベントベースの非同期パターンの例

クラス 操作
System.Activities.WorkflowInvoker InvokeAsync
System.ComponentModel.BackgroundWorker RunWorkerAsync
System.Net.Mail.SmtpClient SendAsync
System.Net.NetworkInformation.Ping SendAsync
System.Net.WebClient DownloadStringAsync

IAsyncResult パターン

.NET において非同期操作を実装する際のもう 1 つのプログラム記法は IAsyncResult パターンです。IAsyncResult は、イベントベースのモデルよりも高度な非同期プログラミング ソリューションです。

IAsyncResult パターンは、Begin メソッドと End メソッドを使用して非同期操作を公開します。Begin メソッドを呼び出して非同期操作を開始し、操作の完了時に呼び出されるデリゲートを渡します。そして、コールバックから、非同期操作の結果を返す End メソッドを呼び出します。または、コールバックを用意する代わりに、操作が完了しているかどうかをポーリングするか、同期をとって操作を待機することができます。

では、例として、Dns.GetHostAddresses メソッドについて考えてみましょう。このメソッドは、ホスト名を受け取り、そのホスト名が解決する IP アドレスの配列を返します。このメソッドの同期バージョンのシグネチャは、次のようになります。

public static IPAddress[] GetHostAddresses(
  string hostNameOrAddress)

このメソッドの非同期バージョンは、次のように公開します。

public static IAsyncResult BeginGetHostAddresses(
  string hostNameOrAddress,
  AsyncCallback requestCallback,
  Object state)

public static IPAddress[] EndGetHostAddresses(
  IAsyncResult asyncResult)

次の例では、BeginGetHostAddresses メソッドと EndGetHostAddresses メソッドを使用して、www.microsoft.com というアドレスの DNS を非同期にクエリしています。

static void Main() {
  Dns.BeginGetHostAddresses(
    "www.microsoft.com",
    result => {
      IPAddress[] addresses = Dns.EndGetHostAddresses(result);
      Console.WriteLine(addresses[0]);
    }, 
    null);
  Console.ReadKey();
}

図 3 に、イベントベースのパターンを使用して、非同期操作を実装するいくつかの .NET クラスを示します。図 2図 3 を比較すると、イベントベースのパターンを実装するクラス、IAsyncResult パターンを実装するクラス、そしてその両方を実装するクラスがあることにお気付きになるでしょう。

図 3 .NET クラスでの IAsyncResult の例

クラス 操作
System.Action BeginInvoke
System.IO.Stream BeginRead
System.Net.Dns BeginGetHostAddresses
System.Net.HttpWebRequest BeginGetResponse
System.Net.Sockets.Socket BeginSend
System.Text.RegularExpressions.MatchEvaluator BeginInvoke
System.Data.SqlClient.SqlCommand BeginExecuteReader
System.Web.DefaultHttpHandler BeginProcessRequest

歴史的に見ると、IAsyncResult パターンは、非同期 API を実装する高パフォーマンスのアプローチとして .NET Framework 1.0 で導入されました。ところが、UI スレッドを操作する場合は別の作業が必要になるうえに、適切に実装しにくく、使用するのが困難でした。イベントベースのパターンは、IAsyncResult によって対処されていなかった UI の側面を容易にするため .NET Framework 2.0 で導入され、UI アプリケーションが 1 つの非同期アプリケーションを起動してそれを操作するというシナリオを中心に使用されています。

タスク パターン

.NET Framework 4 では、非同期操作を表す方法として、新しい型、System.Threading.Tasks.Task が導入されました。タスクでは、次のように、CPU で実行される通常のコンピューター処理を表現できます。

static void Main() {
  Task<double> task = Task.Factory.StartNew(() => { 
    double result = 0; 
    for (int i = 0; i < 10000000; i++) 
      result += Math.Sqrt(i);
    return result;
  });

  Console.WriteLine("The task is running asynchronously...");
  task.Wait();
  Console.WriteLine("The task computed: {0}", task.Result);
}

StartNew メソッドを使用して作成されるタスクは、既定では、スレッド プールでコードを実行するタスクに相当します。ただし、タスクはもっと汎用的で、任意の非同期操作を表現できます。たとえば、サーバーとの通信や、ディスクからのデータ読み取りに相当する操作も表現できます。

TaskCompletionSource は、非同期操作を表すタスクを作成するための、一般的なメカニズムです。TaskCompletionSource は、1 つのタスクにのみ関連付けられます。TaskCompletionSource の SetResult メソッドが呼び出されると、関連付けられているタスクが完了して、そのタスクの結果値が返されます (図 4 参照)。

図 4 TaskCompletionSource の使用

static void Main() {
  // Construct a TaskCompletionSource and get its 
  // associated Task
  TaskCompletionSource<int> tcs = 
    new TaskCompletionSource<int>();
  Task<int> task = tcs.Task;

  // Asynchronously, call SetResult on TaskCompletionSource
  ThreadPool.QueueUserWorkItem( _ => {
    Thread.Sleep(1000); // Do something
    tcs.SetResult(123);
  });

  Console.WriteLine(
    "The operation is executing asynchronously...");
  task.Wait();

  // And get the result that was placed into the task by 
  // the TaskCompletionSource
  Console.WriteLine("The task computed: {0}", task.Result);
}

ここでは、TaskCompletionSource の SetResult を呼び出すために、スレッド プールのスレッドを使用しています。ただし、注意すべき重要な点は、TaskCompletionSource にアクセスできるすべてのコードから SetResult メソッドを呼び出せることです。Button.Click イベントのイベント ハンドラー、なんらかのコンピューター処理を完了するタスク、サーバーが要求に応答したために発生するイベントなど、任意のコードから呼び出せます。

つまり、TaskCompletionSource は、非同期操作を実装する非常に汎用的なメカニズムといえます。

IAsyncResult パターンを変換する

非同期プログラミングにタスクを使用する場合は、古いモデルを使用して公開されている非同期操作と相互運用できることが重要です。TaskCompletionSource は、あらゆる非同期操作をラップしてタスクとして公開することができますが、これに対して Task API は、IAsyncResult パターンをタスクに変換する便利なメカニズムとして、FromAsync メソッドを提供します。

次の例では、FromAsync メソッドを使用して、IAsyncResult ベースの非同期操作である Dns.BeginGetHost Addresses をタスクに変換しています。

static void Main() {
  Task<IPAddress[]> task = 
    Task<IPAddress[]>.Factory.FromAsync(
      Dns.BeginGetHostAddresses, 
      Dns.EndGetHostAddresses,
      "https://www.microsoft.com", null);
  ...
}

FromAsync を使用すると、IAsyncResult パターンの非同期操作が容易にタスクに変換されます。実際には FromAsync は、ThreadPool を利用している TaskCompletionSource の例と同じ方法で実装されます。次に、それと似ている実装方法を簡単に示します。ここでは、GetHostAddresses を直接対象としています。

static Task<IPAddress[]> GetHostAddressesAsTask(
  string hostNameOrAddress) {

  var tcs = new TaskCompletionSource<IPAddress[]>();
  Dns.BeginGetHostAddresses(hostNameOrAddress, iar => {
    try { 
      tcs.SetResult(Dns.EndGetHostAddresses(iar)); }
    catch(Exception exc) { tcs.SetException(exc); }
  }, null);
  return tcs.Task;
}

イベントベースのパターンを変換する

イベントベースの非同期操作は、TaskCompletionSource クラスを使用するタスクに変換することもできます。Task クラスには、このような変換メカニズムは組み込まれていません。イベントベースの非同期パターンはプログラム記法にすぎないため、汎用のメカニズムは実用的ではありません。

次に、イベントベースの非同期操作をタスクに変換する方法を示します。次のコード サンプルでは、URI を受け取って、WebClient.DownloadStringAsync という非同期操作を表すタスクを返すメソッドを示します。

static Task<string> DownloadStringAsTask(Uri address) {
  TaskCompletionSource<string> tcs = 
    new TaskCompletionSource<string>();
  WebClient client = new WebClient();
  client.DownloadStringCompleted += (sender, args) => {
    if (args.Error != null) tcs.SetException(args.Error);
    else if (args.Cancelled) tcs.SetCanceled();
    else tcs.SetResult(args.Result);
  };
  client.DownloadStringAsync(address);
  return tcs.Task;
}

このパターンと、前のセクションのパターンを使用すると、イベントベース、または IAsyncResult ベースの既存の非同期パターンをすべてタスクに変換できます。

タスクの操作と構成

では、非同期操作を表すために、なぜタスクを使用するのでしょう。主な理由は、タスクが非同期操作を簡単に操作および構成できるメソッドを公開しているためです。IAsyncResult やイベントベースのアプローチとは異なり、タスクでは、非同期操作に関連する情報 (操作の実行方法、結果の取得方法など) をすべて管理するオブジェクトが 1 つ提供されます。

タスクが便利なのは、完了を待機できる点です。1 つのタスクの完了を待機したり、一連のタスクすべてが完了するまで待機したり、一連のタスクのうちいずれかのタスクが完了するまで待機したりすることができます。

static void Main() {
  Task<int> task1 = new Task<int>(() => ComputeSomething(0));
  Task<int> task2 = new Task<int>(() => ComputeSomething(1));
  Task<int> task3 = new Task<int>(() => ComputeSomething(2));

  task1.Wait();
  Console.WriteLine("Task 1 is definitely done.");

  Task.WaitAny(task2, task3);
  Console.WriteLine("Task 2 or task 3 is also done.");

  Task.WaitAll(task1, task2, task3);
  Console.WriteLine("All tasks are done.");
}

また、あるタスクが完了すると同時に別のタスクを実行するというように、タスク継続のスケジュールを設定する便利な機能もあります。完了の待機の場合と同様に、特定のタスクが完了したとき、一連のタスクすべてが完了したとき、または一連のタスクのうちいずれかのタスクが完了したときに実行する継続スケジュールを設定できます。

次の例では、www.microsoft.com というアドレスの DNS をクエリするタスクを作成しています。そのタスクが完了すると、次のタスクが開始され、コンソールに結果が出力されます。

static void Main() {
  Task<IPAddress[]> task = 
    Task<IPAddress[]>.Factory.FromAsync(
      Dns.BeginGetHostAddresses, 
      Dns.EndGetHostAddresses,
      "www.microsoft.com", null);

  task.ContinueWith(t => Console.WriteLine(t.Result));
  Console.ReadKey();
}

非同期操作の表現としてのタスクの能力を示す、さらに興味深い例を見てみましょう。図 5 は、2 つの DNS の照合を並列実行する例を示しています。非同期操作をタスクとして表すと、複数の操作の完了を待機するのが容易になります。

図 5 操作の並列実行

static void Main() {
  string[] urls = new[] { "www.microsoft.com", "www.msdn.com" };
  Task<IPAddress[]>[] tasks = new Task<IPAddress[]>[urls.Length];

  for(int i=0; i<urls.Length; i++) {
    tasks[i] = Task<IPAddress[]>.Factory.FromAsync(
      Dns.BeginGetHostAddresses,
      Dns.EndGetHostAddresses,
      urls[i], null);
  }

  Task.WaitAll(tasks);

  Console.WriteLine(
    "microsoft.com resolves to {0} IP addresses. msdn.com resolves to {1}",
    tasks[0].Result.Length,
    tasks[1].Result.Length);
}

それでは、次の 3 つの手順から成るタスクを構成する、もう 1 つの例を見てみましょう。

  1. 複数の HTML ページを並列かつ非同期にダウンロードする
  2. ダウンロードした HTML ページを処理する
  3. 各 HTML ページの情報を集計する

図 6 は、先ほどの DownloadStringAsTask メソッドを利用して、こうしたコンピューター処理がどのように実装されるかを示しています。この実装における注目すべきメリットは、2 つの異なる CountParagraphs メソッドがそれぞれ別のスレッドで実行される点です。マルチコア コンピューターが普及している現状を考えると、コンピューター処理の負荷が高い作業を複数のスレッドに分散するプログラムのパフォーマンスが向上することになります。

図 6 文字列の非同期ダウンロード

static void Main() {
  Task<string> page1Task = DownloadStringAsTask(
    new Uri("https://www.microsoft.com"));
  Task<string> page2Task = DownloadStringAsTask(
    new Uri("http://www.msdn.com"));

  Task<int> count1Task = 
    page1Task.ContinueWith(t => CountParagraphs(t.Result));
  Task<int> count2Task = 
    page2Task.ContinueWith(t => CountParagraphs(t.Result));

  Task.Factory.ContinueWhenAll(
    new[] { count1Task, count2Task },
    tasks => {
      Console.WriteLine(
        "<P> tags on microsoft.com: {0}", 
        count1Task.Result);
      Console.WriteLine(
        "<P> tags on msdn.com: {0}", 
        count2Task.Result);
  });
        
  Console.ReadKey();
}

同期コンテキストでタスクを実行する

特定の同期コンテキストで実行される継続操作のスケジュールを設定できると便利なことがあります。たとえば、UI を備えたアプリケーションでは、UI スレッドで実行される継続操作のスケジュールを設定できると便利です。

同期コンテキストでタスクを操作する最も簡単な方法は、現在のスレッドのコンテキストをキャプチャする TaskScheduler を作成することです。UI スレッド用に TaskScheduler を取得するには、UI スレッドでの実行中に TaskScheduler 型の FromCurrentSynchronizationContext 静的メソッドを呼び出します。

次の例では、www.microsoft.com という Web ページを非同期にダウンロードして、ダウンロードした HTML を、WPF テキスト ボックスの Text プロパティに割り当てています。

void Button_Click(object sender, RoutedEventArgs e) {
  TaskScheduler uiTaskScheduler =
    TaskScheduler.FromCurrentSynchronizationContext()

  DownloadStringAsTask(new Uri("https://www.microsoft.com"))
    .ContinueWith(
       t => { textBox1.Text = t.Result; },
       uiTaskScheduler);
}

Button_Click メソッドの本体は、最終的に UI を更新する非同期コンピューター処理を設定しますが、Button_Click は計算が完了するのを待機しません。このため、UI スレッドはブロックされず、ユーザー インターフェイスの更新と、ユーザーの操作への応答を続行できます。

先ほども述べましたが、.NET Framework 4 より前の非同期操作は、通常、IAsyncResult パターンかイベントベースのパターンのいずれかを使用して公開していました。.NET Framework 4 では、非同期操作の便利な表現の 1 つとして Task クラスを使用できるようになりました。多くの場合、非同期操作をタスクとして表すと簡単に操作および構成できるようになります。非同期プログラミングにタスクを使用している例は、この他にも、code.msdn.microsoft.com/ParExtSamples (英語) からダウンロードできる、ParallelExtensionsExtras のサンプルに含まれています。

Igor Ostrovsky は、マイクロソフトの並列コンピューティング プラットフォーム チームのソフトウェア開発エンジニアであり、igoro.com (英語) において、彼のプログラミングにおける冒険を綴っています。また、.NET のブログ (blogs.msdn.com/pfxteam、英語) で、並列プログラミングについて寄稿しています。

この記事のレビューに協力してくれた同時実行ランタイム チームの技術スタッフに心より感謝いたします。