COM STA/MTA の基礎知識 - スレッドモデルとハングを避ける考え方

· · COM, Windows開発, STA, MTA, スレッド

COM の STA/MTA は、Windows 開発や .NET から COM を触るときに避けて通りにくい基礎知識です。 特に検索で多いのは、UI スレッドがなぜ STA なのか、Apartment をまたぐと何が起きるのか、なぜハングするのか、という疑問です。

目次

  1. まず結論(ひとことで)
  2. Apartment Modelの呼び出しパターン(図)
  3. STA(Single-Threaded Apartment)
  4. MTA(Multi-Threaded Apartment)
  5. STA/MTAはどこで決まるのか
  6. STAを間違えると起きるハングの具体例
  7. ざっくり使い分け
  8. まとめ
  9. 参考資料

COMを使うとき、「どのスレッドで動くか」は避けて通れません。その中心にあるのが Apartment Model(STA/MTA) です。STA/MTAはWindowsの一般的なスレッド概念ではなく、COMオブジェクトの呼び出し規則を決めるためのスレッドモデルです。

この記事では、STAとMTAとCOMの関係を図にしながら、「なぜハングすることがあるのか」までつなげて説明します。

1. まず結論(ひとことで)

  • COMオブジェクトは「どのApartmentに所属するか」で呼び出し規則が決まる
  • STAは 1スレッドに1Apartment、MTAは 複数スレッドで1Apartment と考えると理解しやすい
  • Apartmentを跨ぐ呼び出しは、COMがProxy/Stub経由でマーシャリングする

2. Apartment Modelの呼び出しパターン(図)

COMオブジェクトの呼び出しには、大きく3つのパターンがあります。

2.1. パターン1: 同一STAスレッド内での呼び出し

同じSTAスレッド内なら、直接呼び出しできます。オーバーヘッドなし。

STAスレッド直接呼び出し呼び出し元コードCOMオブジェクト

2.2. パターン2: 同一MTA内での呼び出し

MTA内の複数スレッドからは、どのスレッドからでも直接呼び出しできます。 ただしオブジェクト側はスレッドセーフ設計が必須

MTA(1つのApartment)直接呼び出し直接呼び出しワーカースレッド1COMオブジェクトワーカースレッド2

2.3. パターン3: Apartmentを跨ぐ呼び出し

異なるApartment間では、COMがProxy/Stubを使って転送します。 標準的なインターフェースならCOMランタイムが処理してくれます。

注意: Proxy/Stubは何でも自動で用意されるわけではないのですが、実務では明示的に生成しなくて済む場合がほとんどです。

パターン Proxy/Stubの準備
IDispatch ベース(Automation) 不要。oleaut32.dll が処理
タイプライブラリ登録済み 不要。タイプライブラリマーシャラーが処理
.NET COM Interop 通常は不要。タイプライブラリ経由で動く
IUnknown 直接派生のカスタムIF MIDLでProxy/Stub生成・登録が必要

つまり、MIDLでProxy/Stub生成が必要になるのは、IDispatch を使わず IUnknown 直接派生のインターフェースを作る場合です。 .NETやスクリプト言語から使う一般的なCOMコンポーネントでは、この作業が必要になることは少ないです。

MTAスレッドCOMランタイム(自動)STAスレッド呼び出し転送COMオブジェクトProxyRPC/IPCStub呼び出し元コード

ポイント: Apartmentを跨ぐとマーシャリングのオーバーヘッドが発生します。 高頻度の呼び出しでは性能に影響するため、設計時に考慮が必要です。

2.4. マーシャリングのオーバーヘッド目安

以下は一般的な目安です(実測値ではなく、状況・パラメータの複雑さで大きく変わります)。

呼び出しパターン 目安の時間 相対的な感覚
同一Apartment内(直接) 10〜100ナノ秒 通常の関数呼び出しとほぼ同じ
異なるApartment(同一プロセス) 1〜10マイクロ秒 直接呼び出しの100〜1000倍
異なるプロセス(Out-of-proc) 100〜1000マイクロ秒 直接呼び出しの1万〜10万倍

相対的な比較:

  • 同一Apartment: 1回のメモリアクセス程度
  • 異なるApartment: 1回のシステムコール程度
  • 異なるプロセス: ローカルホストへのネットワーク通信程度

ループで1万回呼ぶような場面では、この差が顕著に効いてきます。

3. STA(Single-Threaded Apartment)

STAは「1スレッド = 1Apartment」というモデルです。

  • そのApartment内のCOMオブジェクトは、基本的にそのスレッドでのみ実行
  • 別スレッドから呼ぶと、COMがメッセージキュー/RPC経由で呼び出しを転送
  • UIスレッド(WinForms/WPF)でよく使われる(UIも「1スレッド親和性+メッセージループ」なので相性が良い)

3.1. なぜUIスレッドでSTAが使われるのか

UIスレッドとSTAは設計が一致しているからです。

  • UIコントロールはスレッドセーフではない ボタンやテキストボックスなどは、生成したスレッドからしか安全に操作できない
  • STAも同じく「1スレッド親和性」 COMオブジェクトは生成したスレッドでのみ直接実行される
  • UIスレッドは必ずメッセージループを回す ウィンドウイベントを処理するために必須。STAの前提(メッセージポンプ)と一致する

だからWinForms/WPFのUIスレッドはデフォルトでSTAになっています。

ポイント: STAはスレッド親和性が高い代わりに、呼び出し元が多いと渋滞しやすい

4. MTA(Multi-Threaded Apartment)

MTAは「複数スレッドで1Apartment」というモデルです。

  • COMオブジェクトは複数スレッドから同時に呼び出される
  • オブジェクト側でスレッドセーフ設計が必須
  • サーバーサイド処理やバックグラウンド処理向き

ポイント: MTAは並列性が高いが、オブジェクト実装の責任が重い

5. STA/MTAはどこで決まるのか

COMのApartmentは、スレッドごとに初期化することで決まります。

  • CoInitialize / CoInitializeEx を呼んだ瞬間に、そのスレッドのApartmentが決まる
  • STA: COINIT_APARTMENTTHREADED
  • MTA: COINIT_MULTITHREADED

5.1. .NETでのSTA/MTA

.NETにも [STAThread] / [MTAThread] 属性や ApartmentState がありますが、これらはCOMのApartment Modelを設定するためのラッパーです。

  • [STAThread]Mainメソッド(エントリポイント)に付ける。COMを使う際にSTAとして初期化される
  • [MTAThread] → 同様にMainメソッド用。MTAとして初期化される
  • Thread.SetApartmentState(ApartmentState.STA)追加で作るスレッド用。スレッド開始前に設定が必要

注意点:

  • [STAThread] があっても、実際にCOMを呼ぶまでは初期化されない(COMを使わないなら効果なし)
  • 追加スレッドには [STAThread] は効かない。Thread.SetApartmentState を使う

つまり、.NETのSTA/MTAはCOMのSTA/MTAそのものであり、COM Interopのために用意された仕組みです。

重要: 後からApartmentを変更することはできません。最初の初期化が全てです。

6. STAを間違えると起きるハングの具体例

次のような構成は、実際にハングを引き起こしやすいです。

6.1. よくある状況

  • バックグラウンドでSTAスレッドを作成してCOMオブジェクトを生成
  • そのスレッドはメッセージループを回していない
  • 別スレッド(STA/MTA問わず)からそのCOMオブジェクトを呼び出す

6.2. 何が起きるのか

STAのCOMオブジェクトへの呼び出しは、そのSTAスレッドで処理されます。呼び出し元がSTAでもMTAでも、別スレッドならCOMがメッセージ/RPCで転送する仕組みです。ところがSTAスレッドがメッセージを処理しない状態だと、呼び出しはずっと待たされ、結果としてハングします。

6.3. 擬似コード(典型的な失敗パターン)

var ready = new AutoResetEvent(false);
var done = new AutoResetEvent(false);

object comObj = null;
var staThread = new Thread(() =>
{
    // STAとして初期化
    CoInitializeEx(IntPtr.Zero, COINIT_APARTMENTTHREADED);

    comObj = new SomeStaComObject();
    ready.Set();

    // メッセージループがないまま待機 -> ここが致命傷
    done.WaitOne();
});

staThread.SetApartmentState(ApartmentState.STA);
staThread.Start();

ready.WaitOne();

// 別スレッド(STA/MTA問わず)から呼ぶと、呼び出しがSTAに転送される
// しかしSTA側はメッセージを処理しないため、ここでハングしやすい
CallComObject(comObj);
COMランタイムSTAスレッドメインスレッドCOMランタイムSTAスレッドメインスレッドメッセージループなしここで詰まっているメッセージで転送するが...WaitOne中なのでメッセージを処理できない呼び出し元も待ち続ける両方が待ち状態 → ハングスレッド開始CoInitializeEx(STA)COMオブジェクト生成ready.Set()done.WaitOne()で待機CallComObject()呼び出しを転送しようとする

要するに、ハングの理由はSTAの2つの前提にあります。

  • COMオブジェクトは生成したSTAスレッドで処理される 別スレッドからの呼び出しは、必ずそのSTAスレッドに転送される
  • その転送を受け取るために、STAスレッドはメッセージポンプを回す 回していないと呼び出しを受け取れない

だから、

  • メッセージを回していないSTAスレッドは呼び出しを受け取れない
  • 受け取れないので呼び出し元が待ち続け、結果としてハングする

一方、UIスレッドはウィンドウイベントを処理するために最初からメッセージループを回しているので、STAの要件を追加実装なしで満たしています。UIスレッドがSTAのCOMオブジェクトを動かす場所として自然な選択肢になるのは、このためです。

6.4. 回避の要点

  • 別スレッドからの呼び出しを受ける場合、STAスレッドはメッセージループを回す必要がある
  • 可能ならUIスレッド上で生成・利用する(UIスレッドは最初からメッセージループがある)
  • STAが不要なら最初からMTAにする

補足: 同一スレッド内だけで完結するなら、常に Application.Run() が必要とは限りません。 ただし、UI系・COM系は別スレッドからの呼び出しが絡むことが多いため、実務上はほぼ必須です。

6.5.「メッセージループを回す」って結局なに?

Win32のUIスレッドがやっている、例のこれです。

while (GetMessage(out var msg, IntPtr.Zero, 0, 0))
{
    TranslateMessage(ref msg);
    DispatchMessage(ref msg);
}

STAでは、別スレッドからの呼び出しが「転送」されてきます。 その転送を受け取って実行に回すのが、このループ(メッセージポンプ)だ、という話です。

6.6. 正しい方向の例(雑に書くとこう)

「バックグラウンドSTAでCOMを使いたい」なら、こういう形になります。

var ready = new AutoResetEvent(false);
object comObj = null;

var staThread = new Thread(() =>
{
    CoInitializeEx(IntPtr.Zero, COINIT_APARTMENTTHREADED);

    comObj = new SomeStaComObject();
    ready.Set();

    // STAスレッドが生きている間はメッセージを回す
    Application.Run();

    CoUninitialize();
});

staThread.SetApartmentState(ApartmentState.STA);
staThread.Start();

ready.WaitOne();
CallComObject(comObj);

(※ CoInitializeEx / CoUninitialize の呼び忘れは普通に事故ります)

6.7. もう一つのハング例: 同期呼び出し中のコールバック

STAは「呼び出しが転送される」だけでなく、状況によっては逆方向(サーバー→クライアント)にコールバックが来ます。中でも同期呼び出し中にコールバックが発生するパターンは、デッドロックの定番です。

COMサーバーUIスレッド(STA)COMサーバーUIスレッド(STA)DoWorkの戻りを待っている(メッセージを処理していない)待機中なのでコールバックを受け取れないコールバックの完了を待っているお互いが相手を待っている → デッドロックDoWork()(同期呼び出し)ProgressCallback()(コールバック)

なぜデッドロックになりやすいのか:

  1. UIスレッドが DoWork()同期呼び出し(ブロッキング)
  2. UIスレッドは戻りを待っている(メッセージを処理していない)
  3. サーバーが ProgressCallback() をUIスレッドに送る
  4. UIスレッドは待機中なのでコールバックを受け取れない
  5. サーバーはコールバックの完了を待っている
  6. お互いが相手を待っている → 永遠に進まない

処理時間の長さは関係ありません。同期呼び出し中にコールバックが来るというパターン自体が問題になりやすいです。

補足: COMには状況によってメッセージを回す・再入する仕組みもあり、コンポーネントや呼び出し形態で挙動が変わります。 必ずデッドロックになるわけではありませんが、このパターンは避けるのが無難です。

7. ざっくり使い分け

  • UIが絡む → STA
  • 大量並列処理 → MTA
  • どちらでもない → 既存ライブラリやCOMサーバーの要求に合わせる

8. まとめ

STA/MTAはCOMのためのスレッドモデルで、STAは1スレッド = 1Apartment、MTAは複数スレッドで1Apartmentという形をとります。Apartmentを跨ぐ呼び出しはCOMがProxy/Stub経由で転送してくれます(標準IF以外はMIDL等での生成・登録が必要)が、そこにはマーシャリングのオーバーヘッドが伴うため、高頻度の呼び出しが想定される場面ではApartment設計を慎重に決めたいところです。

ハングの観点では、「別スレッドからの呼び出しを受けるSTAスレッドは、メッセージポンプを回すことが前提」という一点に尽きます。メッセージを回していないSTAスレッドに呼び出すとハングしやすく、同期呼び出し中にコールバックが来るパターンもデッドロックになりやすい。UIスレッドは「1スレッド親和性」と「メッセージループ」を最初から持っているため、この前提を追加実装なしで満たしており、STAのCOMと相性が良いわけです。

9. 参考資料

  • Apartment Model https://learn.microsoft.com/en-us/windows/win32/com/com-apartments
  • CoInitializeEx https://learn.microsoft.com/en-us/windows/win32/api/objbase/nf-objbase-coinitializeex

この記事のWordファイルをダウンロード

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る