STAスレッドを使用したアプリのメモリリークについて

Last Update: feedback 共有

こんにちは、Japan Developer Support Core チームの近澤です。.NET アプリケーションでメモリリークが発生した際に、STA スレッドが関係している場合があります。今回は、STAスレッドの仕組みと、よくあるメモリリークの原因、その対処方法についてご紹介します。

STA スレッドとは

STA(Single Threaded Apartment)スレッドは、COM コンポーネントでスレッドセーフを明示しない実装でも安全に使えるようにするための仕組みです。

STA スレッドが必要な理由

STA スレッドは、Windows Forms アプリケーションなど UI アプリケーションでよく使用されています。
Windows Forms コントロールの多くは、パフォーマンスや内部で依存しているネイティブ コントロールの互換性の関係から、それ自身はスレッドセーフではありません。また、例えば Windows Forms のクリップボードや Dialog 表示など、内部で COM オブジェクトを利用しているコンポーネントは、マルチスレッドからのアクセスを避けるため、STA スレッドからの利用のみを許可しています。

なお、これらのプロジェクトは、Visual Studio のプロジェクトテンプレートから作成された時点で既定で STA スレッドとしてマークされています。

STA スレッドの設定方法

A. メインスレッドに STAThread 属性を付与

Windows Forms プロジェクトでは、Program.cs に以下のように [STAThread] 属性が記載されています:

1
2
3
4
5
6
7
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}

B. 新しいスレッドを STA として起動

プログラムの途中でスレッドを起動する際は、下記のようにアパートメントを指定します:

1
2
3
4
ThreadStart threadStart = new ThreadStart(MyThreadMethod);
Thread thread = new Thread(threadStart);
thread.SetApartmentState(ApartmentState.STA);
thread.Start();

問題の概要:メモリリークの原因

STA スレッドを使用した場合、前提として、 後述する「メッセージポンプ」が実装されている必要があります。メッセージポンプとは、STA に属する COM コンポーネントを使用する際に、COMのガイドラインに則って、対象の STA スレッドで定期的にウィンドウメッセージを処理する仕組みです。

メッセージポンプが無い場合の問題

STA の外部から STA のオブジェクトへアクセスするためには、ウィンドウメッセージを介する必要があります。メッセージポンプが実装されていない場合、以下の問題が発生します:

  • 外部スレッドからのアクセス不可: STA スレッドがウィンドウメッセージを処理できないため、外部のスレッドから STA スレッドにアクセスできず、外部スレッドがハングアップします。

  • ガベージコレクション時の問題: 開発者が明示的にアクセスしない場合でも、ガベージコレクション(GC)でファイナライザが COM コンポーネントのオブジェクトを破棄する際に同様の問題が発生します。

メモリリーク発生のメカニズム

具体的には、以下のような流れでメモリリークが発生します:

  1. STA スレッドでの COM オブジェクト生成
    メインスレッドに STA 属性が設定され、何らかの COM オブジェクトが生成・利用されます。この COM オブジェクトは STA に属し、外部のアパートメント (スレッド) からは直接操作することはできず、外部から操作するにはウィンドウ メッセージを使用した通信により、STA スレッドに処理を依頼する必要があります。

  2. ガベージコレクションの実行
    アプリケーションが動作し続け、ヒープ上の空き領域が不足すると、ガベージコレクション(GC)が自動実行されます。

  3. ファイナライザスレッドの動作
    GC の処理の一環として、ファイナライザスレッドが解放可能なオブジェクトのファイナライズメソッドを呼び出します。

  4. COM オブジェクトのファイナライズ試行
    COM オブジェクト(厳密には RCW オブジェクト)のファイナライズ処理を実行しようとしますが、このオブジェクトは STA オブジェクトのため、ファイナライザスレッドからは直接操作できず、STA スレッドに処理を依頼します。

  5. メッセージポンプ不備による応答不可
    STA スレッドがメッセージポンプを実行していないため、ファイナライザスレッドからの処理要求に応答できません。

  6. メモリリークの発生
    ファイナライザスレッドは STA スレッドからの応答を待ち続け、ファイナライズが必要なオブジェクトの処理を進められず、オブジェクトが解放されずに蓄積されてメモリリークを引き起こします。

対処方法

A. メッセージポンプの実装

STA スレッド内で以下の処理を定期的に呼び出すことで、メッセージポンプを実装できます:

1
System.Threading.Thread.CurrentThread.Join(0);

Join メソッドの第一引数で待ち時間(ミリ秒)を指定できます。この処理により、ファイナライザスレッドから STA スレッドへのウィンドウメッセージによる処理要求に応答できるようになります。

B. MTA スレッドの使用検討

MTA(Multi Threaded Apartment)スレッドであればメッセージポンプを実装する責務はありません。MTA でも動作可能な COM オブジェクトを使用する場合は、MTA スレッドの使用も検討できます。

参考資料

まとめ

STA スレッドを使用する場合は、適切なメッセージポンプの実装が不可欠です。メッセージポンプが無いと、ガベージコレクション時にファイナライザスレッドがハングアップし、メモリリークが発生する可能性があります。STA スレッドの責務を理解し、適切な実装を行うことで、このような問題を回避できます。


本ブログの内容は弊社の公式見解として保証されるものではなく、開発・運用時の参考情報としてご活用いただくことを目的としています。もし公式な見解が必要な場合は、弊社ドキュメント (https://docs.microsoft.comhttps://support.microsoft.com) をご参照いただくか、もしくは私共サポートまでお問い合わせください。