次の方法で共有


並列コンピューティング

SynchronizationContext こそすべて

Stephen Cleary

マルチスレッド プログラミングは非常に難しく、着手するには膨大な概念やツールの習得が必要です。Microsoft .NET Framework には、開発者を支援するために、SynchronizationContext クラスが用意されています。残念なことに、多くの開発者はこの便利なツールに気が付いてすらいません。

プラットフォームに関係なく、つまり ASP.NET、Windows フォーム、Windows Presentation Foundation (WPF)、Silverlight など、どのプラットフォームであっても、すべての .NET プログラムには SynchronizationContext の概念が含まれるため、すべてのマルチスレッド プログラマにとっては SynchronizationContext を理解して適用することにメリットがあります。

SynchronizationContext の必要性

マルチスレッド プログラムは .NET Framework が登場するはるか前から存在していました。マルチスレッド プログラムでは、あるスレッドから別のスレッドに作業単位を受け渡す必要が生じることがよくあります。Windows プログラムはメッセージ ループを中心とするため、多くのプログラマは組み込みのメッセージ キューを使用して作業単位を受け渡していました。マルチスレッド プログラムで Windows メッセージ キューをこの目的に使用する場合、独自のカスタム Windows メッセージと、そのメッセージを処理するための取り決めを定義する必要がありました。

.NET Framework が最初にリリースされたとき、この共通パターンが標準化されました。当時、.NET でサポートされていた GUI アプリケーションの種類は、Windows フォームだけでした。しかし、フレームワークの設計者は他のモデルの登場も予測し、汎用ソリューションを考案しました。そこで誕生したのが ISynchronizeInvoke です。

ISynchronizeInvoke の基になっているのは、ソース スレッドはデリゲートをターゲット スレッドに対するキューに登録し、必要に応じてそのデリゲートの完了を待機するという考え方です。ISynchronizeInvoke には、現在のコードがターゲット スレッドで実行中かどうかを判断するプロパティも用意されていました (既に実行中であれば、デリゲートをキューに登録する必要はありません)。Windows フォームは ISynchronizeInvoke の実装だけを提供し、パターンは非同期コンポーネントを設計するために開発されていたため、まったく問題はありませんでした。

.NET Framework 2.0 では、抜本的な変更が多数行われました。主な強化点の 1 つは、ASP.NET アーキテクチャに非同期ページを導入したことです。.NET Framework 2.0 より前、すべての ASP.NET 要求は、その要求が完了するまでスレッドを保持しておく必要がありました。このスレッドの使い方は非効率です。Web ページの作成過程では、データベース クエリを使用したり、Web サービスを呼び出したりすることが多く、作成要求を処理するスレッドはこのような操作がすべて完了するまで待機しなければなりません。非同期ページにより、各操作を開始した要求処理スレッドを ASP.NET スレッド プールに返すことができるようになりました。操作が完了したら、ASP.NET スレッド プールの別のスレッドでその要求を完了することになります。

しかし、ISynchronizeInvoke は ASP.NET の非同期ページ アーキテクチャにはあまり適していませんでした。ASP.NET の非同期ページは 1 つのスレッドに関連付けられることはないため、ISynchronizeInvoke パターンを使用して開発した非同期コンポーネントは ASP.NET ページで正しく機能しなくなります。非同期ページは、要求を開始したスレッドに対して作業をキュー登録するのではなく、まだ終了していない操作の "数" を管理して、ページ要求を完了するタイミングを決定します。多くの検討を重ね、慎重に設計した結果、ISynchronizeInvoke は SynchronizationContext に置き換えられることになりました。

SynchronizationContext の概念

ISynchronizeInvoke は、同期が必要かどうかの判断と、あるスレッドから別のスレッドに対して作業単位をキュー登録するという 2 つのニーズを満たしていました。SynchronizationContext は ISynchronizeInvoke を置き換えることを目的に設計されましたが、設計段階後に、そのまま置き換えられるわけではないことがわかりました。

SynchronizationContext の側面の 1 つは、作業単位をコンテキストに対するキューに登録する方法を用意していることです。この作業単位は、特定のスレッドではなく、"コンテキスト" に対するキューに登録されることに注意してください。SynchronizationContext の実装の多くは 1 つの特定のスレッドに関連付けられないため、この違いは重要です。SynchronizationContext には、同期が必要かどうか判断するメカニズムが含まれていません。それは、常に必要かどうかを判断できるとは限らないためです。

SynchronizationContext の 2 つ目の側面は、各スレッドが "現在" コンテキストを持つことです。スレッドのコンテキストは必ずしも一意にはならず、あるスレッドのコンテキストのインスタンスが他のスレッドと共有されることもあります。スレッドが自身の現在コンテキストを変更することもありますが、非常にまれです。

SynchronizationContext の 3 つ目の側面は、終了していない非同期操作の数を管理することです。操作数を管理すると、ASP.NET 非同期ページの使用や、このような操作数を必要とする他のホストの使用が可能になります。ほとんどの場合、現在の SynchronizationContext がキャプチャされると操作数が増加し、キャプチャした SynchronizationContext を使用してコンテキストに完了通知をキュー登録すると操作数が減少します。

SynchronizationContext には他にも側面がありますが、ほとんどのプログラマにとってはそれほど重要ではありません。特に重要な側面を図 1 に示します。

図 1 SynchronizationContext API の側面

// The important aspects of the SynchronizationContext APIclass SynchronizationContext

{

  // Dispatch work to the context.

  void Post(..); // (asynchronously)

  void Send(..); // (synchronously)

  // Keep track of the number of asynchronous operations.

  void OperationStarted();

  void OperationCompleted();

  // Each thread has a current context.

  // If "Current" is null, then the thread's current context is


  // "new SynchronizationContext()", by convention.

  static SynchronizationContext Current { get; }

  static void SetSynchronizationContext(SynchronizationContext);
}

SynchronizationContext の実装

SynchronizationContext では実際の "コンテキスト" が明確には定義されていません。さまざまなフレームワークやホストで独自のコンテキストを自由に定義できます。このようなさまざまな実装とその制限事項を理解しておくと、SynchronizationContext の概念で保証される点とされない点を明確に区別できます。ここでは、このような実装のいくつかについて簡単に説明します。

WindowsFormsSynchronizationContext (System.Windows.Forms.dll: System.Windows.Forms): Windows フォーム アプリケーションは、UI コントロールを作成するスレッドごとに、WindowsFormsSynchronizationContext を現在コンテキストとして作成およびインストールします。この SynchronizationContext は、UI コントロールの ISynchronizeInvoke のメソッドを使用します。このメソッドは、基になる Win32 メッセージ ループにデリゲートを渡します。WindowsFormsSynchronizationContext のコンテキストは、1 つの UI スレッドです。

WindowsFormsSynchronizationContext にキュー登録されたすべてのデリゲートは、一度に 1 つずつ実行されます。つまり、キューに登録された順番に従って、固有の UI スレッドによって実行されます。現在の実装では、UI スレッドごとに WindowsFormsSynchronizationContext を 1 つ作成します。

DispatcherSynchronizationContext (WindowsBase.dll: System.Windows.Threading): WPF アプリケーションと Silverlight アプリケーションは DispatcherSynchronizationContext を使用します。この実装は、"通常" の優先度で UI スレッドの Dispatcher クラスにデリゲートをキュー登録します。この SynchronizationContext は、スレッドが Dispatcher.Run を呼び出してそのスレッドの Dispatcher ループを開始する際に、現在コンテキストとしてインストールされます。DispatcherSynchronizationContext のコンテキストは、1 つの UI スレッドです。

DispatcherSynchronizationContext にキュー登録されたすべてのデリゲートは、キューに登録された順番に従って、一度に 1 つずつ固有の UI スレッドによって実行されます。現在の実装では、トップレベルのウィンドウごとに DispatcherSynchronizationContext を 1 つ作成します (すべてのトップレベルのウィンドウで同一の基になる Dispatcher を共有している場合でも同様)。

既定 (ThreadPool) の SynchronizationContext (mscorlib.dll: System.Threading): 既定の SynchronizationContext は、既定で作成される SynchronizationContext オブジェクトです。原則として、スレッドの現在 SynchronizationContext が null の場合、暗黙のうちに既定の SynchronizationContext を保持します。

既定の SynchronizationContext は、その非同期デリゲートを ThreadPool にキュー登録しますが、同期デリゲートについては呼び出し元スレッドで直接実行します。したがって、既定の SynchronizationContext のコンテキストは、すべての ThreadPool スレッドと、Send を呼び出すすべてのスレッドを対象とします。コンテキストは、Send を呼び出すスレッドを "借用" して、デリゲートが完了するまでそのスレッドをコンテキスト内に含めます。このような意味で、既定のコンテキストにはプロセス内のあらゆるスレッドが含まれている可能性があります。

既定の SynchronizationContext は、コードが ASP.NET によってホストされていない限り ThreadPool スレッドに適用されます。また、明示的な子スレッド (Thread クラスのインスタンス) が独自の SynchronizationContext を設定していない限り、このような子スレッドにも暗黙のうちに適用されます。したがって、UI アプリケーションには通常、UI スレッドを対象とする UI SynchronizationContext と、ThreadPool スレッドを対象とする既定の SynchronizationContext の 2 つの同期コンテキストが存在します。

既定の SynchronizationContext では、イベントベースの非同期コンポーネントの多くが期待どおりに動作しません。悪名高い例に、ある BackgroundWorker から別の BackgroundWorker を開始する UI アプリケーションがあります。各 BackgroundWorker は、RunWorkerAsync を呼び出すスレッドの SynchronizationContext をキャプチャおよび使用し、RunWorkerAsync の RunWorkerCompleted イベントをそのコンテキスト内で後から実行します。BackgroundWorker が 1 つの場合、通常は UI ベースの SynchronizationContext をキャプチャするため、RunWorkerCompleted イベントは、RunWorkerAsync がキャプチャした UI コンテキスト内で実行されます (図 2 参照)。

image: A Single BackgroundWorker in a UI Context

図 2 UI コンテキスト内に 1 つ存在する BackgroundWorker

しかし、BackgroundWorker の DoWork ハンドラー内で別の BackgroundWorker を開始すると、BackgroundWorker が入れ子になり、入れ子になった BackgroundWorker は UI の SynchronizationContext をキャプチャしません。DoWork ハンドラーは、既定の SynchronizationContext を使用して ThreadPool スレッドによって実行されます。この場合、入れ子になった RunWorkerAsync は既定の SynchronizationContext をキャプチャするため、UI スレッドではなく ThreadPool スレッドでその RunWorkerCompleted イベントを実行します (図 3 参照)。

image: Nested BackgroundWorkers in a UI Context

図 3 UI コンテキスト内の入れ子になった BackgroundWorker

既定では、コンソール アプリケーションと Windows サービスのすべてのスレッドは、既定の SynchronizationContext しか所持しません。このため、一部のイベント ベースの非同期コンポーネントでエラーが発生します。1 つの解決策として、明示的な子スレッドを作成し、この子スレッドに SynchronizationContext をインストールする方法があります。この方法を使用すると、イベント ベースの非同期コンポーネントにコンテキストを提供できるようになります。SynchronizationContext の実装については、今回の記事で扱う範囲を超えているため説明は割愛しますが、Nito.Async ライブラリ (nitoasync.codeplex.com、英語) の ActionThread クラスを SynchronizationContext の汎用実装として使用することもできます。

AspNetSynchronizationContext (System.Web.dll: System.Web (内部クラス)): ASP.NET SynchronizationContext は、スレッド プールのスレッドでページ コードを実行する際に、そのスレッドにインストールされます。キャプチャした AspNetSynchronizationContext に対するキューにデリゲートを登録すると、AspNetSynchronizationContext は元のページの ID とカルチャを復元して、デリゲートを直接実行します。Post を呼び出すことで "非同期" にデリゲートをキュー登録した場合でも、デリゲートは直接呼び出されます。

AspNetSynchronizationContext のコンテキストの概念は複雑です。非同期ページの有効期間中に、コンテキストは ASP.NET スレッド プールからのスレッドを 1 つだけ備えた状態で始まります。非同期要求が行われたらと、コンテキストにはスレッドが含まれなくなります。非同期要求が完了するときに、完了ルーチンを実行しているスレッド プールのスレッドがそのコンテキストに含められます。コンテキストに含められるスレッドは、要求を開始したスレッドと同じ場合もありますが、多くの場合は、操作の完了時点で使用されていない任意のスレッドになります。

同じアプリケーションの複数の操作が同時に完了すると、AspNetSynchronizationContext は、常に、完了した操作を一度に 1 つずつ実行します。操作が実行されるスレッドは特定されませんが、実行するスレッドには元のページの ID とカルチャが渡されます。

一般的な例として、非同期 Web ページ内から使用する WebClient があります。DownloadDataAsync は、現在の SynchronizationContext をキャプチャし、DownloadDataAsync の DownloadDataCompleted イベントをそのコンテキスト内で後から実行します。ページの実行が始まると、ASP.NET はスレッドの 1 つを割り当て、そのページのコードを実行します。ページは、DownloadDataAsync を呼び出してから結果を返すこともあります。このような場合、ASP.NET が完了していない非同期操作の数を管理しているため、そのページの実行は完了していないと認識されます。WebClient オブジェクトが要求したデータをダウンロードしたときに、スレッド プールのスレッドで通知を受けます。このスレッドは、キャプチャしたコンテキスト内で DownloadDataCompleted イベントを発生します。コンテキストは同一スレッドに存在したままですが、イベント ハンドラーは適切な ID とカルチャで実行されます。

SynchronizationContext の実装に関する注意事項

SynchronizationContext は、多種多様なフレームワーク内で動作するコンポーネントの作成手段を提供します。BackgroundWorker と WebClient は、Windows フォーム、WPF、Silverlight、コンソール、および ASP.NET のすべてのアプリケーションで正常に機能するコンポーネントの例です。ただし、このような再利用可能なコンポーネントを設計する際は、いくつか考慮が必要な点があります。

一般に、SynchronizationContext の各種実装では、等値かどうかを比較できません。つまり、ISynchronizeInvoke.InvokeRequired に相当する機能がありません。ただし、これは重大な欠点というわけではありません。コードで複数のコンテキストを処理しようとするのではなく、必ず既知のコンテキスト内で実行すれば、コードが簡潔で検証しやすくなります。

SynchronizationContext のすべての実装で、デリゲートの実行順序やデリゲートの同期順序が保証されるわけではありません。UI ベースの SynchronizationContext の実装ではどちらも保証されますが、ASP.NET の SynchronizationContext では同期順序のみが保証されます。既定の SynchronizationContext では、実行と同期のいずれの順序も保証されません。

SynchronizationContext のインスタンスとスレッドは、1 対 1 には対応しません。WindowsFormsSynchronizationContext のインスタンスはスレッドと 1 対 1 に対応しますが (SynchronizationContext.CreateCopy が呼び出されていない場合のみ)、他の実装では対応しないこともあります。一般に、コンテキストのインスタンスが特定のスレッドで実行されることを想定しないことをお勧めします。

最後に、SynchronizationContext.Post メソッドは必ずしもが非同期である必要はありません。ほとんどの実装ではこのメソッドを非同期に実装していますが、AspNetSynchronizationContext は例外です。そのため、再入性について予期しない問題が発生することがあります。このようなさまざまな実装を図 4 にまとめます。

図 4 SynchronizationContext の実装に関するまとめ

  デリゲートの実行に特定のスレッドを使用する 排他的 (デリゲートが一度に 1 つずつ実行される) 順序付き (キューへの登録順にデリゲートが実行される) Send でデリゲートを直接呼び出す Post でデリゲートを直接呼び出す
Windows フォーム はい はい はい UI スレッドから呼び出す場合 呼び出さない
WPF/Silverlight はい はい はい UI スレッドから呼び出す場合 呼び出さない
既定 いいえ いいえ いいえ 常に 呼び出さない
ASP.NET いいえ はい いいえ 常に 常に

AsyncOperationManager と AsyncOperation

.NET Framework の AsyncOperationManager クラスと AsyncOperation クラスは、SynchronizationContext を抽象化する軽量のラッパーです。AsyncOperationManager クラスは、AsyncOperation の初回作成時に現在の SynchronizationContext をキャプチャし、現在の SynchronizationContext が null であれば既定の SynchronizationContext に置き換えます。AsyncOperation クラスは、キャプチャした SynchronizationContext にデリゲートを非同期にポストします。

ほとんどのイベントベースの非同期コンポーネントは、その実装内で AsyncOperationManager クラスと AsyncOperation クラスを使用します。2 つのクラスは、完了時点が決まっている非同期操作、つまり、ある時点から始まり、別の時点にイベントを使って完了する非同期操作で適切に動作します。完了時点が決まっていない非同期通知もあります。たとえば、ある時点から始まり、無期限に継続するサブスクリプションです。このような操作は、SynchronizationContext を直接キャプチャおよび使用します。

新しいコンポーネントでは、イベントベースの非同期パターンを使用しないことをお勧めします。Visual Studio Async CTP (Community Technology Preview) には、タスクベースの非同期パターンを説明するドキュメントが付属しており、このパターンでは、SynchronizationContext を使用してイベントを発生させずに、コンポーネントから Task オブジェクトと Task<TResult> オブジェクトを返します。タスクベースの API は、.NET における非同期プログラミングの将来像と言えます。

SynchronizationContext のライブラリ サポートの例

BackgroundWorker や WebClient などの単純なコンポーネントは、SynchronizationContext のキャプチャと使用を隠蔽するため、そのままでも暗黙に移植可能です。ただし、多くのライブラリは、SynchronizationContext をもっとわかりやすい方法で使用します。SynchronizationContext を使用する API をライブラリで公開すると、フレームワークに依存しなくなるだけでなく、上級エンド ユーザーに拡張ポイントも提供します。

ここで紹介しているライブラリと現在の SynchronizationContext は、ExecutionContext の一部と見なされます。スレッドの ExecutionContext をキャプチャするすべてのシステムは、現在の SynchronizationContext をキャプチャします。ExecutionContext が復元されると、通常は SynchronizationContext も復元されます。

Windows Communication Foundation (WCF): UseSynchronizationContext WCF には、ServiceBehaviorAttribute と CallbackBehaviorAttribute という、サーバーとクライアントの動作の構成に使用する 2 つの属性があり、どちらの属性にもブール型の UseSynchronizationContext プロパティがあります。このプロパティの既定値は true なので、通信チャネルの作成時に現在の SynchronizationContext がキャプチャされ、このキャプチャされた SynchronizationContext を使用してコントラクト メソッドがキューに登録されます。

通常は、これがまさに必要な動作です。つまり、サーバーは既定の SynchronizationContext を使用し、クライアントのコールバックは適切な UI の SynchronizationContext を使用します。ただし、クライアントのコールバックを呼び出すサーバー メソッドをクライアントから呼び出す場合など、再入性が必要な場合は、問題が発生することがあります。このような場合、UseSynchronizationContext プロパティを false に設定して、WCF による SynchronizationContext が自動的な使用を無効にすることができます。

ここでは、WCF が SynchronizationContext を使用する方法を簡単に説明しただけです。詳細については、2007 年 11 月号の MSDN マガジンの記事「WCF での同期コンテキスト」(msdn.microsoft.com/magazine/cc163321) を参照してください。

Windows Workflow Foundation (WF): WorkflowInstance.SynchronizationContext 当初、WF ホストは WorkflowSchedulerService と派生型を使用して、スレッドでワークフロー アクティビティのスケジュールを設定する方法を制御していました。.NET Framework 4 へのアップグレードの一環として、WorkflowInstance クラスと、このクラスから派生した WorkflowApplication クラスに、SynchronizationContext プロパティが含められました。

ホスティング プロセスで独自の WorkflowInstance を作成すると、SynchronizationContext を直接設定されることがあります。SynchronizationContext は、WorkflowInvoker.InvokeAsync メソッドからも使用されます。このメソッドは、現在の SynchronizationContext をキャプチャして内部の WorkflowApplication に渡します。その後、この SynchronizationContext を使用して、ワークフローの完了イベントとワークフロー アクティビティをポストします。

Task Parallel Library (TPL: タスク並列ライブラリ): TaskScheduler.FromCurrentSynchronizationContext と CancellationToken.Register TPL では、タスク オブジェクトを作業単位として使用し、TaskScheduler を通じて実行します。既定の TaskScheduler の動作は既定の SynchronizationContext と同様で、タスクを ThreadPool に対してキュー登録します。TPL が提供する TaskScheduler には、タスクを SynchronizationContext に対してキュー登録するものもあります。入れ子になったタスクを使用して、UI を更新しながら進捗状況を報告することもできます (図 5 参照)。

図 5 UI を更新しながら進捗状況を報告する

private void button1_Click(object sender, EventArgs e)
{
  // This TaskScheduler captures SynchronizationContext.Current.
  TaskScheduler taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
  // Start a new task (this uses the default TaskScheduler, 
  // so it will run on a ThreadPool thread).
  Task.Factory.StartNew(() =>
  {
    // We are running on a ThreadPool thread here.

  
    ; // Do some work.


  // Report progress to the UI.
    Task reportProgressTask = Task.Factory.StartNew(() =>
      {
        // We are running on the UI thread here.

        ; // Update the UI with our progress.
      },
      CancellationToken.None,
      TaskCreationOptions.None,
      taskScheduler);
    reportProgressTask.Wait();
  
    ; // Do more work.
  });
}

CancellationToken クラスは、.NET Framework 4 でのあらゆる種類のキャンセルに使用します。既存の形式のキャンセルと統合されるよう、このクラスではキャンセルの要求時に呼び出すデリゲートを登録できます。デリゲートを登録するときに、SynchronizationContext を渡すことができます。キャンセルが要求されると、CancellationToken はデリゲートを直接実行する代わりに SynchronizationContext に対してキュー登録します。

Microsoft Reactive Extensions (Rx): ObserveOn、SubscribeOn、および SynchronizationContextScheduler Rx は、イベントをデータのストリームとして扱うライブラリです。ObserveOn 演算子は、SynchronizationContext を通じてイベントをキュー登録し、SubscribeOn 演算子は、SynchronizationContext を通じてこのようなイベントの "サブスクリプション" をキューに登録します。通常、ObserveOn 演算子は入力イベントで UI を更新するために使用し、SubscribeOn 演算子は、UI オブジェクトで発生したイベントを利用するために使用します。

Rx には、作業単位をキュー登録する独自の方法として、IScheduler インターフェイスもあります。また、SynchronizationContext に対してキュー登録する SynchronizationContextScheduler という IScheduler の実装もあります。

Visual Studio Async CTP: await、ConfigureAwait、SwitchTo、および EventProgress<T> Microsoft Professional Developers Conference 2010 では、Visual Studio が非同期コード変換をサポートすることが発表されました。既定では、待機点で現在の SynchronizationContext をキャプチャし、キャプチャした SynchronizationContext を使用して待機後に再開します (正確には、現在の SynchronizationContext が null でない限りキャプチャします。null の場合は、現在の TaskScheduler をキャプチャします)。

private async void button1_Click(object sender, EventArgs e)
{
  // SynchronizationContext.Current is implicitly captured by await.
  var data = await webClient.DownloadStringTaskAsync(uri);

  // At this point, the captured SynchronizationContext was used to resume
  // execution, so we can freely update UI objects.
}

ConfigureAwait を使用すると、既定の SynchronizationContext のキャプチャ動作を回避できます。つまり、flowContext パラメーターに false を渡すと、待機後に実行を再開する際に SynchronizationContext が使用されなくなります。SynchronizationContext のインスタンスには、SwitchTo という拡張メソッドもあります。このため、SwitchTo メソッドを呼び出して結果を待機すると、任意の非同期メソッドを別の SynchronizationContext に切り替えることができます。

Visual Studio Async CTP では、非同期操作から進捗状況を報告する共通パターンとして、IProgress<T> インターフェイスとその実装である EventProgress<T> が導入されています。このクラスは、作成時に現在の SynchronizationContext をキャプチャして、そのコンテキスト内で ProgressChanged イベントを発生させます。

このようなサポートに加えて、void を返す非同期メソッドが導入され、メソッドの開始時には非同期操作の数が増加し、終了時には減少します。そのため、void を返す非同期メソッドは、トップレベルの非同期操作と同様に機能します。

制限事項と保証事項

SynchronizationContext を理解することは、すべてのプログラマーにとって有益です。フレームワークに依存しない既存のコンポーネントは、イベントの同期に SynchronizationContext を使用します。ライブラリでは、高度な柔軟性を実現するために SynchronizationContext を公開することができます。SynchronizationContext の制限事項と保証事項を理解すれば、このようなクラスをさらにうまく作成して使用できるようになります。

Stephen Cleary は、マルチスレッドについて初めて耳にしたときからずっとこの概念に興味を抱いており、Syracuse News、R. R. Donnelley、BlueScope Steel などの大規模顧客向けに、業務上重要なマルチスレッド システムを多数手掛けてきました。また、ミシガン北部の自宅付近で開催される .NET ユーザー グループ、バーキャンプ、および Day of .NET のイベントで、主にマルチスレッド関連のトピックについて定期的に講演しています。彼のブログは nitoprograms.com (英語) で読むことができます。

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