Warning
STA スレッドでの非同期/待機呼び出しとブロック呼び出しによるデッドロック リスク:マネージド コード (.NET) では、STA メッセージ ループは COM 呼び出しディスパッチに不可欠です。
Task.Wait()、Task.Result、Thread.Sleep()、またはManualResetEvent.WaitOne()で STA スレッドをブロックすると、COM コールバックとアパートメント間呼び出しが完了しなくなり、デッドロックが発生します。
// ❌ DEADLOCK — blocks the STA message loop
[STAThread]
static void Main()
{
var result = GetDataAsync().Result; // Deadlock: awaited continuation
// can't post back to this STA thread
}
// ✅ Correct — use async entry point or pump messages
[STAThread]
static async Task Main()
{
var result = await GetDataAsync(); // continuation resumes on STA via SynchronizationContext
}
awaitを使用できない場合は、STA スレッドの生待機プリミティブではなく、CoWaitForMultipleHandles (ネイティブ) またはディスパッチャー フレーム/メッセージ ポンプ (マネージド) を使用します。
シングル スレッド アパートメント (アパートメント モデル プロセス) を使用すると、同時に実行される複数のオブジェクトを処理するためのメッセージ ベースのパラダイムが提供されます。 これにより、スレッドを許可し、時間のかかる操作が完了するまで待機しながら、別のスレッドの実行を許可することで、より効率的なコードを記述できます。
アパートメント モデル プロセスとして初期化され、ウィンドウ メッセージを取得してディスパッチするプロセス内の各スレッドは、シングル スレッドのアパートメント スレッドです。 各スレッドは、独自のアパートメント内に存在します。 アパートメント内では、マーシャリングを行わずにインターフェイス ポインターを渡すことができるため、1 つのシングルスレッド アパートメント スレッド内のすべてのオブジェクトが直接通信します。
すべてのオブジェクトが同じスレッドで実行され、したがって同期実行が必要な関連オブジェクトの論理グループは、同じシングルスレッド アパートメント スレッド上に存在する可能性があります。 ただし、アパートメント モデル オブジェクトを複数のスレッドに配置することはできません。 他のスレッド内のオブジェクトへの呼び出しは、所有しているスレッドのコンテキスト内で行う必要があるため、分散 COM はプロキシを呼び出すときに自動的にスレッドを切り替えます。
プロセス間モデルとスレッド間モデルは似ています。 同じプロセス内の別のアパートメント (別のスレッド上) のオブジェクトにインターフェイス ポインターを渡す必要がある場合は、異なるプロセス内のオブジェクトがプロセス境界を越えてポインターを渡すために使用するのと同じマーシャリング モデルを使用します。 標準マーシャリング オブジェクトへのポインターを取得することで、プロセス間で行うのと同じ方法で、スレッド境界 (アパートメント間) を越えてインターフェイス ポインターをマーシャリングできます。 (インターフェイス ポインターは、アパートメント間で渡されるときにマーシャリングする必要があります)。
シングル スレッド アパートメントの規則は単純ですが、慎重に従う必要があります。
- すべてのオブジェクトは、(シングルスレッドアパートメント内で) 1つのスレッドでのみ稼働する必要があります。
- 各スレッドの COM ライブラリを初期化します。
- アパートメント間でオブジェクトを渡すときに、オブジェクトへのすべてのポインターをマーシャリングします。
- 各シングルスレッド アパートメントには、同じプロセス内の他のプロセスとアパートメントからの呼び出しを処理するためのメッセージ ループが必要です。 オブジェクトのないシングル スレッド アパートメント (クライアントのみ) には、一部のアプリケーションで使用されるブロードキャスト メッセージをディスパッチするためのメッセージ ループも必要です。
- DLL ベースまたはインプロセス オブジェクトでは、COM 初期化関数は呼び出されません。代わりに、レジストリの InprocServer32 キーの下に、スレッド モデルを ThreadingModel 名前付き値に登録します。 また、アパートメント対応オブジェクトは DLL エントリ ポイントを慎重に記述する必要があります。 インプロセス サーバーのスレッド処理には、特有の注意事項があります。 詳細については、「In-Process Server Threading Issues」を参照してください。
複数のオブジェクトを 1 つのスレッドで使用できますが、複数のスレッドで稼働できるアパートメント モデル オブジェクトはありません。
クライアント プロセスまたはアウトプロセス サーバーの各スレッドは、CoInitializeを呼び出すか、CoInitializeEx呼び出して、dwCoInit パラメーターのCOINIT_APARTMENTTHREADEDを指定する必要があります。 メイン アパートメントは、最初 CoInitializeEx を呼び出すスレッドです。 インプロセス サーバーの詳細については、「In-Process Server Threading Issues」を参照してください。
オブジェクトへのすべての呼び出しは、そのスレッド (そのアパートメント内) で行う必要があります。 別のスレッドから直接オブジェクトを呼び出すことを禁止します。このフリースレッド方式でオブジェクトを使用すると、アプリケーションに問題が発生する可能性があります。 このルールの意味は、アパートメント間で渡されるときに、オブジェクトへのすべてのポインターをマーシャリングする必要があるということです。 COM には、この目的のために次の 2 つの関数が用意されています。
- CoMarshalInterThreadInterfaceInStream は、呼び出し元に返されるストリーム オブジェクトにインターフェイスをマーシャリングします。
- CoGetInterfaceAndReleaseStream ストリーム オブジェクトからインターフェイス ポインターをアンマーサルし、解放します。
これらの関数は、CoMarshalInterface 関数および CoUnmarshalInterface 関数の呼び出しをラップするものであり、これらの関数では MSHCTX_INPROC フラグを使用する必要があります。
一般に、マーシャリングは COM によって自動的に実行されます。 たとえば、プロキシのメソッド呼び出しでパラメーターとしてインターフェイス ポインターを別のアパートメント内のオブジェクトに渡す場合や、CoCreateInstance呼び出すと、COM はマーシャリングを自動的に実行します。 ただし、アプリケーション ライターが通常の COM メカニズムを使用せずにアパートメント間でインターフェイス ポインターを渡す特殊なケースでは、ライターはマーシャリングを手動で処理する必要があります。
プロセス内の 1 つのアパートメント (アパートメント 1) にインターフェイス ポインターがあり、別のアパートメント (アパートメント 2) がその使用を必要とする場合、アパートメント 1 は、インターフェイスをマーシャリングするために、CoMarshalInterThreadInterfaceInStream を呼び出す必要があります。 この関数によって作成されるストリームはスレッド セーフであり、アパートメント 2 からアクセスできる変数に格納する必要があります。 アパートメント 2 では、このストリームを CoGetInterfaceAndReleaseStreamに渡してインターフェイスのマーシャリングを解除する必要があり、インターフェイスにアクセスできるプロキシへのポインターが返されます。 メイン アパートメントは、クライアントが COM のすべての作業を完了するまで存続する必要があります (「In-Process Server Threading Issues」で説明されているように、一部のインプロセス オブジェクトがメイン アパートメントに読み込まれるためです)。 この方法でスレッド間で 1 つのオブジェクトが渡された後、インターフェイス ポインターをパラメーターとして渡すのは非常に簡単です。 このように、分散 COM は、アプリケーションのマーシャリングとスレッド切り替えを行います。
同じプロセス内の他のプロセスやアパートメントからの呼び出しを処理するには、各シングルスレッド アパートメントにメッセージ ループが必要です。 つまり、スレッドの作業関数には GetMessage/DispatchMessage ループが必要です。 スレッド間の通信に他の同期プリミティブが使用されている場合は、メッセージとスレッド同期イベントの両方を待機するために、MsgWaitForMultipleObjects 関数を使用できます。 この関数のドキュメントには、この種の組み合わせループの例があります。
COM は、各シングル スレッド アパートメントの Windows クラス "OleMainThreadWndClass" を使用して非表示ウィンドウを作成します。 オブジェクトの呼び出しは、この非表示ウィンドウへのウィンドウ メッセージとして受信されます。 そのオブジェクトが属するアパートメントがメッセージを取得してディスパッチしたとき、非表示ウィンドウはそのメッセージを受け取ります。 ウィンドウ プロシージャは、オブジェクトの対応するインターフェイス メソッドを呼び出します。
複数のクライアントが 1 つのオブジェクトを呼び出すと、呼び出しはメッセージ キューに格納され、そのアパートメントがメッセージを取得してディスパッチするたびに、オブジェクトは呼び出しを受け取ります。 呼び出しは COM によって同期され、呼び出しは常にオブジェクトのアパートメントに属するスレッドによって配信されるため、オブジェクトのインターフェイス実装では同期を提供する必要はありません。 シングル スレッド アパートメントでは、IMessageFilter を実装して、呼び出しをキャンセルしたり、必要に応じてウィンドウ メッセージを受信したりできます。
オブジェクトは、インターフェイス メソッドの実装の 1 つがメッセージを取得してディスパッチしたり、別のスレッドに ORPC 呼び出しを行ったりした場合に再入力できるため、別の呼び出しが (同じアパートメントによって) オブジェクトに配信されます。 OLE は、同じスレッドでの再入を防ぐものではありませんが、スレッド セーフを提供するのに役立ちます。 これは、ウィンドウ プロシージャがメッセージの処理中にメッセージを取得してディスパッチする場合に再入力できる方法と同じです。 ただし、アウトプロセスのシングルスレッド アパートメント サーバーを呼び出し、そのサーバーが別のシングルスレッド アパートメント サーバーを呼び出すと、最初のサーバーに再入することができます。
関連トピック
-
スレッド モデル の選択