COM STA/MTA の基礎知識 - スレッドモデルとハングを避ける考え方
· 小村 豪 · COM, Windows開発, STA, MTA, スレッド
COM の STA/MTA は、Windows 開発や .NET から COM を触るときに避けて通りにくい基礎知識です。 特に検索で多いのは、UI スレッドがなぜ STA なのか、Apartment をまたぐと何が起きるのか、なぜハングするのか、という疑問です。
目次
- まず結論(ひとことで)
- Apartment Modelの呼び出しパターン(図)
- STA(Single-Threaded Apartment)
- MTA(Multi-Threaded Apartment)
- STA/MTAはどこで決まるのか
- STAを間違えると起きるハングの具体例
- ざっくり使い分け
- まとめ
- 参考資料
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スレッド内なら、直接呼び出しできます。オーバーヘッドなし。
flowchart LR
subgraph STA[STAスレッド]
Caller[呼び出し元コード]
Obj[COMオブジェクト]
Caller -->|直接呼び出し| Obj
end
2.2. パターン2: 同一MTA内での呼び出し
MTA内の複数スレッドからは、どのスレッドからでも直接呼び出しできます。 ただしオブジェクト側はスレッドセーフ設計が必須。
flowchart LR
subgraph MTA[MTA(1つのApartment)]
Thread1[ワーカースレッド1]
Thread2[ワーカースレッド2]
Obj[COMオブジェクト]
Thread1 -->|直接呼び出し| Obj
Thread2 -->|直接呼び出し| Obj
end
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コンポーネントでは、この作業が必要になることは少ないです。
flowchart LR
subgraph STA[STAスレッド]
StaCaller[呼び出し元コード]
end
subgraph RT[COMランタイム(自動)]
Proxy[Proxy]
RPC[RPC/IPC]
Stub[Stub]
Proxy --> RPC --> Stub
end
subgraph MTA[MTAスレッド]
MtaObj[COMオブジェクト]
end
StaCaller -->|呼び出し| Proxy
Stub -->|転送| MtaObj
ポイント: 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);
sequenceDiagram
participant Main as メインスレッド
participant STA as STAスレッド
participant COM as COMランタイム
Main->>STA: スレッド開始
STA->>STA: CoInitializeEx(STA)
STA->>STA: COMオブジェクト生成
STA->>Main: ready.Set()
STA->>STA: done.WaitOne()で待機
Note over STA: メッセージループなし<br/>ここで詰まっている
Main->>COM: CallComObject()
COM->>STA: 呼び出しを転送しようとする
Note over COM: メッセージで転送するが...
Note over STA: WaitOne中なので<br/>メッセージを処理できない
Note over Main: 呼び出し元も待ち続ける
Note over Main,STA: 両方が待ち状態 → ハング
要するに、ハングの理由は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は「呼び出しが転送される」だけでなく、状況によっては逆方向(サーバー→クライアント)にコールバックが来ます。中でも同期呼び出し中にコールバックが発生するパターンは、デッドロックの定番です。
sequenceDiagram
participant UI as UIスレッド(STA)
participant Server as COMサーバー
UI->>Server: DoWork()(同期呼び出し)
Note over UI: DoWorkの戻りを待っている<br/>(メッセージを処理していない)
Server->>UI: ProgressCallback()(コールバック)
Note over UI: 待機中なので<br/>コールバックを受け取れない
Note over Server: コールバックの完了を待っている
Note over UI,Server: お互いが相手を待っている → デッドロック
なぜデッドロックになりやすいのか:
- UIスレッドが
DoWork()を同期呼び出し(ブロッキング) - UIスレッドは戻りを待っている(メッセージを処理していない)
- サーバーが
ProgressCallback()をUIスレッドに送る - UIスレッドは待機中なのでコールバックを受け取れない
- サーバーはコールバックの完了を待っている
- お互いが相手を待っている → 永遠に進まない
処理時間の長さは関係ありません。同期呼び出し中にコールバックが来るというパターン自体が問題になりやすいです。
補足: 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
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
Windowsアプリ 外注・受託開発を依頼する前に整理したいこと
Windowsアプリの外注・受託開発を依頼する前に、既存ソフト改修、装置連携、COM/ActiveX、配布・更新、保守の整理ポイントを解説します。
COM/OCX/ActiveX開発でハマる登録とbitnessの罠
COM、OCX、ActiveX開発でハマりやすい32bit/64bit、Visual Studio 2022、regsvr32/Regasm、管理者権限、HKCR、STA/MTAを実務目線で整理します。
Reg-Free COMとは - 登録不要でCOMを使う仕組み
Reg-Free COM の基本、アクティベーション コンテキストとマニフェストの役割、メリット、限界、実務での判断軸を整理します。
Excel帳票出力の作り方 - COM/Open XML/テンプレート
Excel 帳票出力は、Excel を自動操作するのか、xlsx を直接生成するのか、既存 VBA を残すのかで設計がかなり変わります。Windows アプリや業務システムでの帳票出力を前提に、方式選定の基準とおすすめ構成を整理します。
COM / ActiveX / OCX とは何か - 違いと関係をまとめて解説
COMとは何か、ActiveXとは何か、OCXとは何かを、違いと関係、OLEとのつながり、どこで使われるのか、今どう捉えるべきかまで実務目線で整理します。
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
ActiveX / 移行テーマ
COM / ActiveX / OCX を残すか、包むか、置き換えるかを整理するトピックです。
UI スレッド / タイマーテーマ
WPF / WinForms、UI スレッド、async/await、タイマー設計を整理するトピックです。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
技術相談・設計レビュー
STA / MTA、メッセージループ、マーシャリングの整理は、実装前の責務分割やスレッド境界レビューに直結します。
既存資産活用・移行支援
COM を含む既存資産を扱うときに避けにくい基礎なので、既存資産活用・移行支援 の相談とも相性がよいです。
著者プロフィール
記事の著者プロフィールページです。
小村 豪
合同会社小村ソフト 代表
Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。
公開リンク