こんにちは、Visual Studio サポート チームです。
今回は、.NET Framework のコンソール アプリケーションで STA 属性を指定したスレッドから、XmlSerializer クラスを利用するなどして直接、または間接的に COM コンポーネントが利用される場合の注意事項についてご案内します。
注意事項
STA に属する COM コンポーネントを作成した場合は、COM のガイドラインに則り、対象の STA スレッドでは定期的にメッセージ ポンプを動作させてウィンドウ メッセージを処理する必要があります。メッセージ ポンプの処理を実装していないと、対象 STA の外部からは COM コンポーネントにアクセスすることができません。このため、特に .NET Framework のコンソール アプリケーションでは、ファイナライザー スレッドが対象の STA スレッドと通信できずにハング アップしてしまい、メモリ リークなどの問題を引き起こす可能性があります。
STA スレッドがメッセージ ポンプを実装する必要がある点については以下のドキュメントに解説がありますのでご参照ください。
[OLE] OLE スレッド モデルの概要としくみ
https://support.microsoft.com/ja-jp/help/150777/info-descriptions-and-workings-of-ole-threading-models
具体例
.NET Framework の XmlSerializer クラスでは、コンストラクタの特定のオーバーロード (*1) を利用すると、内部でアセンブリを生成してキャッシュする処理が行われます。この処理では内部的に COM を利用しており、STA スレッドから生成すれば STA に、MTA スレッドから生成すれば MTA に属するオブジェクトが生成されます。
(*1) XmlSerializer クラスの特定のコンストラクタのオーバーロードでアセンブリ生成が行われる動作については以下のドキュメントに記載があります。
XmlSerializer Class
https://msdn.microsoft.com/en-us/library/system.xml.serialization.xmlserializer(v=vs.110).aspx
----
Dynamically Generated Assemblies
To increase performance, the XML serialization infrastructure dynamically generates assemblies to serialize and deserialize specified types. The infrastructure finds and reuses those assemblies. This behavior occurs only when using the following constructors:
XmlSerializer.XmlSerializer(Type)
XmlSerializer.XmlSerializer(Type,String)
If you use any of the other constructors, multiple versions of the same assembly are generated and never unloaded, which results in a memory leak and poor performance. The easiest solution is to use one of the previously mentioned two constructors.
----
ここで、ある程度長い期間動作するコンソール アプリケーションにおいて、Main メソッドで [STAThread] 属性を指定して XmlSerializer を使用しているケースを想像してください。
XmlSerializer を特定のコンストラクタで生成すると、前述のとおり STA に属する COM オブジェクトが生成されます。このアプリケーションは長期間動作し続けますが、いずれ、ガベージ コレクション (GC) が実行され、GC の機能の一部であるファイナライザー スレッドはファイナライズ可能なオブジェクトのファイナライズ処理を実行します。
このファイナライズ可能なオブジェクトの中には、前述の XmlSerializer がアセンブリのキャッシュに利用する COM オブジェクト (厳密にはそのマネージ ラッパーである RCW オブジェクト) も含まれますが、このオブジェクトは STA で生成された場合、当該 STA スレッド上でのみ動作するよう COM 基盤によって制御されていることから、ファイナライザー スレッドなど STA の外部のスレッドから直接アクセスすることはできません。STA の外部からのアクセスは COM 基盤によって制御され、ウィンドウ メッセージを介して STA スレッドに処理がディスパッチされます。
STA スレッドはファイナライザー スレッドからウィンドウ メッセージで通知される処理要求を受信できる必要がありますが、コンソール アプリケーションなどでメッセージ ポンプを実装していない場合、この処理要求を受け付けることができず、ファイナライザー スレッドは応答を待ち続けてハング アップした状態となります。
結果として、ファイナライズ可能オブジェクトの終了処理が進まないため、メモリ リーク等の問題につながる場合があります。
再現コード
上述の現象は、以下のようなサンプル コードで確認することができます。
[STAThread]
static void Main(string[] args)
{
XmlSerializer serializer = new XmlSerializer(typeof(MyClass), "http://www.microsoft.com");
serializer = null;
GC.Collect();
for (;;)
{
System.Threading.Thread.Sleep(100);
}
}
サンプル コードをビルドして実行し、WinDbg などのデバッガを使用してファイナライザー スレッドの状態を確認してみます。
マネージ スレッドの一覧から、ファイナライザー スレッドを確認します。5 番スレッドがファイナライザー スレッドです。
0:009> !sos.threads
ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception
0 1 83c 0062f7b0 26020 Preemptive 0259CB5C:00000000 0062aa58 1 STA
5 2 2ef4 0063eb28 2b220 Preemptive 00000000:00000000 0062aa58 0 MTA (Finalizer)
ファイナライザー スレッドの状態を、コール スタックから確認します。
以下では、左端からフレーム番号、ベース ポインター、リターン アドレス、モジュール名!関数名 を表示しています。
関数は下から上に向かってコールされています。
15 番フレームから始まる RCW のクリーンアップ処理の過程で、03 番フレームにみられるような、アパートメントを跨いだ処理の呼び出しを行っており、応答を待っている状態であることがわかります。
# 以下は .NET Framework 4.7 を利用している場合の例ですが、.NET Framework のバージョンによって使用される関数が異なる場合があります。
0:009> ~5k
# ChildEBP RetAddr
00 0463ee10 77362bf3 ntdll!NtWaitForMultipleObjects+0xc
01 0463efa4 770195bb KERNELBASE!WaitForMultipleObjectsEx+0x103
02 0463effc 76feec6d combase!MTAThreadWaitForCall+0xdb
03 (Inline) -------- combase!MTAThreadDispatchCrossApartmentCall+0xaf5
04 (Inline) -------- combase!CSyncClientCall::SwitchAptAndDispatchCall+0xbd4
05 0463f1a8 76fef80b combase!CSyncClientCall::SendReceive2+0xcbd
06 (Inline) -------- combase!SyncClientCallRetryContext::SendReceiveWithRetry+0x29
07 (Inline) -------- combase!CSyncClientCall::SendReceiveInRetryContext+0x29
08 0463f204 76feda65 combase!DefaultSendReceive+0x8b
09 0463f2fc 76f344b5 combase!CSyncClientCall::SendReceive+0x3a5
0a (Inline) -------- combase!CClientChannel::SendReceive+0x7c
0b 0463f328 777067e2 combase!NdrExtpProxySendReceive+0xd5
0c (Inline) -------- RPCRT4!NdrpProxySendReceive+0x21
0d 0463f570 76f35e20 RPCRT4!NdrClientCall2+0x4a2
0e 0463f590 7703120f combase!ObjectStublessClient+0x70
0f 0463f5a0 76f989e1 combase!ObjectStubless+0xf
10 0463f630 76f98a99 combase!CObjectContext::InternalContextCallback+0x1e1
11 0463f684 73bfeff6 combase!CObjectContext::ContextCallback+0x69
12 0463f784 73bff0ca clr!CtxEntry::EnterContext+0x252
13 0463f7bc 73bff10b clr!RCW::EnterContext+0x3a
14 0463f7e0 73bfeed3 clr!RCWCleanupList::ReleaseRCWListInCorrectCtx+0xbc
15 0463f83c 73bfd7f8 clr!RCWCleanupList::CleanupAllWrappers+0x14d
16 0463f88c 73bfdac8 clr!SyncBlockCache::CleanupSyncBlocks+0xd0
17 0463f89c 73bfd7e7 clr!Thread::DoExtraWorkForFinalizer+0x75
18 0463f8cc 73bd1e09 clr!FinalizerThread::FinalizerThreadWorker+0xba
19 0463f8e0 73bd1e73 clr!ManagedThreadBase_DispatchInner+0x71
1a 0463f984 73bd1f40 clr!ManagedThreadBase_DispatchMiddle+0x7e
1b 0463f9e0 73cba825 clr!ManagedThreadBase_DispatchOuter+0x5b
1c (Inline) -------- clr!ManagedThreadBase_NoADTransition+0x2a
1d 0463fa08 73cba8ef clr!ManagedThreadBase::FinalizerBase+0x33
1e 0463fa44 73be5dc1 clr!FinalizerThread::FinalizerThreadStart+0xd4
1f 0463fadc 75a68744 clr!Thread::intermediateThreadProc+0x55
20 0463faf0 779e582d KERNEL32!BaseThreadInitThunk+0x24
21 0463fb38 779e57fd ntdll!__RtlUserThreadStart+0x2f
22 0463fb48 00000000 ntdll!_RtlUserThreadStart+0x1b
対応方法
STA スレッドに、外部からの処理要求に応答できるようメッセージ ポンプを実装します。
当該スレッドのループ処理の内部などで、定期的に以下の処理を実行することでメッセージ ポンプを動作させることができます。
System.Threading.Thread.CurrentThread.Join(0);
この他、前述のサンプル コードのように特に STA を指定しなくても問題ない場合は、[STAThread] を指定せずに、既定の MTA スレッドとして利用することでも問題を回避することができます。
なお、Windows Form アプリケーションなど UI を持つものは既定でメッセージ ポンプが組み込まれているため、通常、本稿で取り上げたような問題が起こることはありません。