更新履歴(6件・最終更新 2026年08月22日)
この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。
- レビュー指摘に対応し、今日追加した図のうち幅が大きすぎたものを縦向きの構成に直し、一部の図とキャプションの表現を本文の記述に合わせて正確にしました。本文の文章は変えていません。
- Apartmentの決まり方、呼び出しの転送、ハングに至る流れ、回避策の分岐などを図でも追えるように、Mermaid図を16点追加し、既存の図に通し番号のキャプションを付けました(地の文500〜750字につき1図の規約に合わせたものです)。本文の文章は変えていません。
- 記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
- 外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
- STAハングの再現コードで、再現しなかった場合もプロセスが終わらない状態になっていたのを直しました。STAスレッドは`done.WaitOne()`で待つフォアグラウンドスレッドなので、`obj.AnyMethod()`が返ってきた場合(agileなクラスだった、呼び出しが転送されなかった等)は誰も`done`を立てず、プロセスが終了しません。再現したときと再現しなかったときで、外から見える症状がどちらも「プロセスが終わらない」になり、再現条件を確かめる道具が条件の成否を隠していました。`try`/`finally`で`done.Set()`と`staThread.Join()`を必ず通すようにしています。
- ハングする理由が複数箇所に散っていたのを6.2に集約し、重複した説明を図への参照に置き換えました。擬似コードだった再現例を、`CoInitializeEx`の宣言込みで`Type.GetTypeFromProgID`を使う実際に動く形に書き直し、再現条件と確認方法を表で明示しました。用語表と判断表を追加し、応答時間の数値は出典のある実測値ではない旨と、自分で測るための計測コードを添えました。
- 初版公開
この記事を引用する(DOI: 10.5281/zenodo.21589582)
この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。
小村 豪(2026)「COM STA/MTA の基礎知識 - スレッドモデルとハングを避ける考え方」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21589582 https://comcomponent.com/blog/2026/01/31/000-sta-mta-com-relationship/
- DOI(最新版)
- 10.5281/zenodo.21589582
- DOI(この版)
- 10.5281/zenodo.21732607
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の関係を図にしながら、「なぜハングすることがあるのか」までつなげて説明します。
flowchart TB
accTitle: Apartment Modelの位置づけ
accDescr: STA/MTAはWindowsの一般的なスレッド概念ではなく、COMオブジェクトの呼び出し規則を決めるためのスレッドモデルであるApartment Modelの2つの形であることを示す図。
com["COMオブジェクトの呼び出し規則"] --> am["Apartment Model"]
am --> sta["STA"]
am --> mta["MTA"]
am -.-> note["Windows一般のスレッド概念ではない"]
図1: STA/MTAは、COMオブジェクトの呼び出し規則を決めるApartment Modelの2つの形。
この記事の知識マップ
COMのSTA(Single-Threaded Apartment)とMTA(Multi-Threaded Apartment)は、どのスレッドからコンポーネントを呼んでよいかを決めるApartment Modelの2つの形です。STAは1スレッドに1Apartmentで、Apartmentを跨ぐ呼び出しはProxy/Stub経由でマーシャリングされます。別スレッドから呼ばれるSTAはメッセージループを回していないと呼び出しの転送を受け取れずハングし、同期呼び出し中にコールバックが来るパターンもデッドロックになりやすい構造です。MTAは複数スレッドで1Apartmentを共有する代わりに、オブジェクト側にスレッドセーフ設計を要求します。.NETの[STAThread]属性は、このApartment初期化を設定するラッパーにすぎません。
flowchart LR
accTitle: COM STA/MTAの知識マップ
accDescr: STAとMTAというCOMのApartmentモデルが、メッセージループ・マーシャリング・Proxy/Stubとどう関係し、メッセージループの不在がSTAのハングやデッドロックにつながる仕組みを示す図
sta["STA(Single-Threaded Apartment)"]
mta["MTA(Multi-Threaded Apartment)"]
com["COM(コンポーネントオブジェクトモデル)"]
com_apartment_model["COMアパートメントモデル(STA/MTA)"]
coinitializeex["CoInitializeEx"]
message_loop["メッセージループ"]
proxy_stub["Proxy/Stub"]
thread_safe_com_object["スレッドセーフなCOMオブジェクト設計"]
windows_forms["Windows Forms"]
stathread_attribute[".NETの[STAThread]/[MTAThread]属性"]
threadingmodel_registry["ThreadingModelレジストリ値"]
com_automation["Automation"]
idispatch["IDispatch"]
midl["MIDL"]
sta_hang["STAのハング(呼び出し転送の停止)"]
msgwaitformultipleobjects["MsgWaitForMultipleObjects"]
sta_callback_deadlock["同期呼び出し中のコールバックによるデッドロック"]
dotnet[".NET(Core以降)"]
com_marshaling["マーシャリング"]
com -->|"利用する"| com_apartment_model
com_apartment_model -->|"前提とする"| coinitializeex
sta -->|"前提とする"| message_loop
sta -.->|"利用する"| proxy_stub
mta -->|"前提とする"| thread_safe_com_object
windows_forms -.->|"利用する"| sta
windows_forms -->|"利用する"| message_loop
stathread_attribute -->|"利用する"| coinitializeex
threadingmodel_registry -.->|"前提とする"| sta
com_automation -->|"利用する"| idispatch
proxy_stub -.->|"で構成できる"| midl
sta -.->|"原因になり得る"| sta_hang
message_loop -->|"防止する"| sta_hang
msgwaitformultipleobjects -.->|"防止する"| sta_hang
sta -.->|"原因になり得る"| sta_callback_deadlock
dotnet -.->|"原因になり得る"| sta_hang
mta -.->|"利用する"| proxy_stub
stathread_attribute -->|"前提とする"| com_apartment_model
proxy_stub -->|"実装を担う"| com_marshaling
com_automation -->|"実装を担う"| com_marshaling
図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全20件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle
1. まず結論(ひとことで)
- COMオブジェクトは「どのApartmentに所属するか」で呼び出し規則が決まる
- STAは 1スレッドに1Apartment、MTAは 複数スレッドで1Apartment と考えると理解しやすい
- Apartmentを跨ぐ呼び出しは、COMがProxy/Stub経由でマーシャリングする
flowchart TB
accTitle: STAとMTAとApartment跨ぎの関係
accDescr: STAは1スレッドに1Apartment、MTAは複数スレッドで1Apartmentという形をとり、Apartmentを跨ぐ呼び出しはCOMがProxy/Stub経由でマーシャリングするという結論を示す図。
sta["STA(1スレッドに1Apartment)"] -->|"Apartmentを跨ぐ呼び出し"| m["Proxy / Stub経由でマーシャリング"]
mta["MTA(複数スレッドで1Apartment)"] -->|"Apartmentを跨ぐ呼び出し"| m
図2: 所属するApartmentが呼び出し規則を決め、Apartmentを跨ぐときだけマーシャリングが挟まる。
2. Apartment Modelの呼び出しパターン(図)
COMオブジェクトの呼び出しには、大きく3つのパターンがあります。
flowchart TB
accTitle: 呼び出しの3つのパターン
accDescr: COMオブジェクトの呼び出しには、同一STAスレッド内での呼び出し、同一MTA内での呼び出し、Apartmentを跨ぐ呼び出しという3つのパターンがあることを示す図。
caller["COMオブジェクトの呼び出し"] --> p1["パターン1: 同一STAスレッド内"]
caller --> p2["パターン2: 同一MTA内"]
caller --> p3["パターン3: Apartmentを跨ぐ"]
図3: 呼び出しパターンは3つに分かれ、どれに当たるかでオーバーヘッドと注意点が変わる。
2.1. パターン1: 同一STAスレッド内での呼び出し
同じSTAスレッド内なら、直接呼び出しできます。オーバーヘッドなし。
flowchart LR
subgraph STA[STAスレッド]
Caller[呼び出し元コード]
Obj[COMオブジェクト]
Caller -->|直接呼び出し| Obj
end
図4: 同じSTAスレッド内の呼び出しは直接呼び出しになり、オーバーヘッドが無い。
2.2. パターン2: 同一MTA内での呼び出し
MTA内の複数スレッドからは、どのスレッドからでも直接呼び出しできます。 ただしオブジェクト側はスレッドセーフ設計が必須。
flowchart LR
subgraph MTA[MTA(1つのApartment)]
Thread1[ワーカースレッド1]
Thread2[ワーカースレッド2]
Obj[COMオブジェクト]
Thread1 -->|直接呼び出し| Obj
Thread2 -->|直接呼び出し| Obj
end
図5: 同一MTA内ならどのスレッドからも直接呼び出せるが、オブジェクト側にスレッドセーフ設計が要る。
2.3. パターン3: Apartmentを跨ぐ呼び出し
異なるApartment間では、COMがProxy/Stubを使って転送します。 標準的なインターフェースならCOMランタイムが処理してくれます。
この先の表には、COM 特有の言葉が説明なしで出てきます。先に 1 行ずつ書いておきます。
| 用語 | 意味 |
|---|---|
| マーシャリング | Apartment やプロセスの境界を越えるときに、呼び出しと引数を「そのまま渡せる形」へ詰め替えて運ぶことです。境界の向こうで元の形に戻されます |
| Proxy / Stub | マーシャリングを担当する 1 組の部品です。呼び出す側に立つのが Proxy(本物のふりをして呼び出しを受け取る)、呼ばれる側に立つのが Stub(受け取った呼び出しを本物のオブジェクトへ渡す)です |
IDispatch |
メソッド名を文字列で問い合わせて、番号で呼び出すための COM インターフェースです。スクリプト言語や VBA から COM を使えるのは、この仕組みがあるためです |
| Automation | IDispatch と、そこで使える限られたデータ型(BSTR、VARIANT など)を前提にした COM の使い方の総称です。この範囲に収まっていると、マーシャリングは OS 側の oleaut32.dll が引き受けます |
| タイプライブラリ | インターフェースの形(メソッド、引数の型)を機械可読な形で書いたデータです。.tlb ファイル単体か、DLL / EXE に埋め込まれた形で置かれます |
| タイプライブラリマーシャラー | タイプライブラリを読んで、その場でマーシャリングを行う COM の標準機能です。専用の Proxy/Stub を作らなくても済むのはこれのおかげです |
| MIDL | インターフェース定義(.idl)から Proxy/Stub のコードなどを生成する、Microsoft のコンパイラです |
注意: Proxy/Stubは何でも自動で用意されるわけではないのですが、実務では明示的に生成しなくて済む場合がほとんどです。
| パターン | Proxy/Stubの準備 |
|---|---|
IDispatch ベース(Automation) |
不要。oleaut32.dll が処理 |
| タイプライブラリ登録済み | 不要。タイプライブラリマーシャラーが処理 |
| .NET COM Interop | 通常は不要。タイプライブラリ経由で動く |
IUnknown 直接派生のカスタムIF |
MIDLでProxy/Stub生成・登録が必要 |
つまり、MIDLでProxy/Stub生成が必要になるのは、IDispatch を使わず IUnknown 直接派生のインターフェースを作る場合です。
.NETやスクリプト言語から使う一般的なCOMコンポーネントでは、この作業が必要になることは少ないです。
flowchart TB
accTitle: Proxy/Stubを自分で用意するかどうかの分かれ目
accDescr: IDispatchベースやタイプライブラリマーシャラーなどCOM標準のマーシャリングで賄える場合はProxy/Stubの生成は不要で、それで賄えないIUnknown直接派生のカスタムインターフェースのときだけMIDLでProxy/Stubの生成と登録が必要になるという分かれ目を示す図。
q{"標準のマーシャリングで賄えるか"} -->|"はい"| auto["Proxy/Stubの生成は不要"]
q -->|"いいえ"| midl["MIDLでProxy/Stubを生成・登録"]
auto -.-> how["IDispatchやタイプライブラリが処理"]
auto -.-> few["実務ではこちらの場合がほとんど"]
図6: Proxy/Stubを明示的に生成するのは、標準のマーシャリングで賄えないIUnknown直接派生のカスタムインターフェースのとき。
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
図7: Apartmentを跨ぐ呼び出しは、COMランタイムが自動で用意するProxy・RPC・Stubを経由して転送される。
ポイント: Apartmentを跨ぐとマーシャリングのオーバーヘッドが発生します。 高頻度の呼び出しでは性能に影響するため、設計時に考慮が必要です。
2.4. マーシャリングのオーバーヘッド目安
以下は一般的な目安です(実測値ではなく、状況・パラメータの複雑さで大きく変わります)。
| 呼び出しパターン | 目安の時間 | 相対的な感覚 |
|---|---|---|
| 同一Apartment内(直接) | 10〜100ナノ秒 | 通常の関数呼び出しとほぼ同じ |
| 異なるApartment(同一プロセス) | 1〜10マイクロ秒 | 直接呼び出しの100〜1000倍 |
| 異なるプロセス(Out-of-proc) | 100〜1000マイクロ秒 | 直接呼び出しの1万〜10万倍 |
相対的な比較:
- 同一Apartment: 1回のメモリアクセス程度
- 異なるApartment: 1回のシステムコール程度
- 異なるプロセス: ローカルホストへのネットワーク通信程度
ループで1万回呼ぶような場面では、この差が顕著に効いてきます。
この数値の扱いについて
上の表は 桁(オーダー)の感覚をつかむためのもので、出典のある実測値ではありません。 公開されている統一ベンチマークがあるわけでもないので、この数値そのものを設計判断の根拠に使わないでください。
一方で、「同一 Apartment 内は直接呼び出し、境界を越えると必ずマーシャリングが挟まる」という構造は、Microsoft のドキュメントに書かれている仕様 です。Single-Threaded Apartments の項には、同じ Apartment 内ならインターフェースポインタをマーシャリングなしで渡せること、Apartment をまたぐ場合は同一プロセス内であってもプロセス間と同じマーシャリングの仕組みを使うこと、そして呼び出しが隠しウィンドウ(OleMainThreadWndClass)へのウィンドウメッセージとして届くことが明記されています。桁が変わるのはこの構造が理由 で、数値そのものは環境と引数の複雑さで動きます。
自分のケースで判断したいときは、同じインターフェースを同一 Apartment から呼んだ場合と、別 Apartment から呼んだ場合で測り比べるのが確実です。
flowchart TB
accTitle: 桁が変わる構造上の理由
accDescr: 同一Apartment内は直接呼び出しなのに対し、Apartmentを跨ぐと同一プロセス内でも必ずマーシャリングが挟まり、宛先がSTAの場合は呼び出しが隠しウィンドウへのウィンドウメッセージとして届くという、桁が変わる構造上の理由を示す図。
same["同一Apartment内の呼び出し"] --> direct["直接呼び出し"]
cross["Apartmentを跨ぐ呼び出し"] --> marshal["必ずマーシャリングが挟まる"]
marshal -.-> msg["STA宛は隠しウィンドウへ届く"]
図8: 桁が変わるのは、境界を越えると必ずマーシャリングが挟まるという仕様上の構造が理由。
// C#。同じ呼び出しを N 回繰り返して 1 回あたりの時間を出す
static void Measure(string label, Action call, int iterations = 100_000)
{
call(); // 初回の遅延(JIT、プロキシ生成、接続確立)を計測から外す
var sw = System.Diagnostics.Stopwatch.StartNew();
for (int i = 0; i < iterations; i++)
{
call();
}
sw.Stop();
double perCallNs = sw.Elapsed.TotalMilliseconds * 1_000_000.0 / iterations;
Console.WriteLine($"{label}: {perCallNs:F1} ns/call");
}
引数の少ないメソッドと、文字列や配列を渡すメソッドの両方で測ると、「マーシャリング量で効き方が変わる」ことも見えます。
3. STA(Single-Threaded Apartment)
STAは「1スレッド = 1Apartment」というモデルです。
- そのApartment内のCOMオブジェクトは、基本的にそのスレッドでのみ実行
- 別スレッドから呼ぶと、COMがメッセージキュー/RPC経由で呼び出しを転送
- UIスレッド(WinForms/WPF)でよく使われる(UIも「1スレッド親和性+メッセージループ」なので相性が良い)
flowchart TB
accTitle: STAの実行モデル
accDescr: STAではApartment内のCOMオブジェクトは基本的に生成したスレッドでのみ実行され、別スレッドからの呼び出しはCOMがメッセージキューやRPC経由でそのスレッドへ転送することを示す図。
obj["STAのCOMオブジェクト"] --> own["生成したスレッドでのみ実行"]
other["別スレッドからの呼び出し"] --> fwd["メッセージキュー / RPC経由で転送"]
fwd --> own
図9: STAのオブジェクトは持ち主のスレッドでだけ動き、よそからの呼び出しは転送されて届く。
3.1. なぜUIスレッドでSTAが使われるのか
UIスレッドとSTAは設計が一致しているからです。
- UIコントロールはスレッドセーフではない ボタンやテキストボックスなどは、生成したスレッドからしか安全に操作できない
- STAも同じく「1スレッド親和性」 COMオブジェクトは生成したスレッドでのみ直接実行される
- UIスレッドは必ずメッセージループを回す ウィンドウイベントを処理するために必須。STAの前提(メッセージポンプ)と一致する
だからWinForms/WPFのUIスレッドはデフォルトでSTAになっています。
flowchart TB
accTitle: UIスレッドとSTAの設計の一致
accDescr: UIコントロールは生成したスレッドからしか安全に操作できず、STAも1スレッド親和性で、UIスレッドが必ず回すメッセージループはSTAの前提であるメッセージポンプと一致するため、UIスレッドは既定でSTAになることを示す図。
ui["UIコントロールの1スレッド親和性"] --> match["設計が一致"]
sta["STAの1スレッド親和性"] --> match
loop["UIスレッドのメッセージループ"] --> pre["STAの前提(メッセージポンプ)"]
pre --> match
match --> def["UIスレッドは既定でSTA"]
図10: 1スレッド親和性とメッセージループという2点で設計が一致するから、UIスレッドはSTAになっている。
ポイント: STAはスレッド親和性が高い代わりに、呼び出し元が多いと渋滞しやすい。
4. MTA(Multi-Threaded Apartment)
MTAは「複数スレッドで1Apartment」というモデルです。
- COMオブジェクトは複数スレッドから同時に呼び出される
- オブジェクト側でスレッドセーフ設計が必須
- サーバーサイド処理やバックグラウンド処理向き
ポイント: MTAは並列性が高いが、オブジェクト実装の責任が重い。
flowchart TB
accTitle: MTAの実行モデル
accDescr: MTAは複数スレッドで1つのApartmentを共有し、COMオブジェクトは複数スレッドから同時に呼び出されるため、オブジェクト側にスレッドセーフ設計が必須になることを示す図。
mta["MTA(複数スレッドで1Apartment)"] --> par["複数スレッドから同時に呼び出される"]
par --> safe["オブジェクト側のスレッドセーフ設計が必須"]
mta -.-> use["サーバーサイドやバックグラウンド処理向き"]
図11: MTAは並列に呼べる代わりに、安全性の責任がオブジェクト実装側に移る。
5. STA/MTAはどこで決まるのか
COMのApartmentは、スレッドごとに初期化することで決まります。
CoInitialize/CoInitializeExを呼んだ瞬間に、そのスレッドのApartmentが決まる- STA:
COINIT_APARTMENTTHREADED - MTA:
COINIT_MULTITHREADED
flowchart TB
accTitle: Apartmentが決まる瞬間
accDescr: スレッドがCoInitializeまたはCoInitializeExを呼んだ瞬間にそのスレッドのApartmentが決まり、COINIT_APARTMENTTHREADEDならSTA、COINIT_MULTITHREADEDならMTAになることを示す図。
th["スレッド"] --> init["CoInitialize / CoInitializeExを呼ぶ"]
init -->|"COINIT_APARTMENTTHREADED"| sta["STAに決まる"]
init -->|"COINIT_MULTITHREADED"| mta["MTAに決まる"]
図12: Apartmentは、スレッドごとに初期化を呼んだ瞬間の指定で決まる。
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を変更することはできません。最初の初期化が全てです。
flowchart TB
accTitle: .NETでApartmentを設定する場所
accDescr: MainメソッドにはSTAThreadやMTAThreadの属性を付け、追加で作るスレッドには開始前にThread.SetApartmentStateで設定し、どちらも最初の初期化でApartmentが確定して後から変更できないことを示す図。
main["Mainメソッド"] --> attr["〔STAThread〕/〔MTAThread〕属性"]
newth["追加で作るスレッド"] --> setap["開始前にThread.SetApartmentState"]
attr --> fixed["最初の初期化でApartmentが確定"]
setap --> fixed
fixed -.-> nochange["後から変更できない"]
図13: エントリポイントは属性、追加スレッドはSetApartmentStateで、いずれも最初の一回で確定する。
6. STAを間違えると起きるハングの具体例
次のような構成は、実際にハングを引き起こしやすいです。
6.1. よくある状況
- バックグラウンドでSTAスレッドを作成してCOMオブジェクトを生成
- そのスレッドはメッセージループを回していない
- 別スレッド(STA/MTA問わず)からそのCOMオブジェクトを呼び出す
flowchart TB
accTitle: ハングを起こしやすい構成
accDescr: バックグラウンドで作ったSTAスレッドがCOMオブジェクトを生成したのにメッセージループを回しておらず、そこへ別スレッドからそのCOMオブジェクトを呼び出すという、ハングを起こしやすい構成を示す図。
bg["バックグラウンドのSTAスレッド"] --> gen["COMオブジェクトを生成"]
bg --> noloop["メッセージループを回していない"]
other["別スレッド(STA / MTA問わず)"] -->|"呼び出す"| gen
図14: 「ループを回さないSTAが持つオブジェクトを、別スレッドから呼ぶ」という組み合わせが危ない。
6.2. 何が起きるのか
ハングの理由は、STAの2つの前提に集約できます。この節が理由の説明で、以降の節では繰り返しません。
- COMオブジェクトは、生成したSTAスレッドで処理される
呼び出し元がSTAでもMTAでも、別スレッドからの呼び出しは必ずそのSTAスレッドへ転送されます。転送は、COMがそのApartmentに作る隠しウィンドウ(ウィンドウクラス
OleMainThreadWndClass)へのウィンドウメッセージとして届きます - その転送を受け取るには、STAスレッドがメッセージポンプを回している必要がある Microsoft のドキュメントでも「各STAは、他のプロセスや同一プロセス内の他のApartmentからの呼び出しを処理するために、メッセージループを持たなければならない」と明記されています
したがって、メッセージを回していないSTAスレッドは呼び出しを受け取れず、呼び出し元は返事を待ち続け、結果としてハングします。
flowchart TB
accTitle: ハングに至る流れ
accDescr: 別スレッドからの呼び出しは生成したSTAスレッドへ隠しウィンドウへのウィンドウメッセージとして転送されるが、STAスレッドがメッセージポンプを回していないと受け取れず、呼び出し元が返事を待ち続けてハングする流れを示す図。
caller["別スレッドからの呼び出し"] --> fwd["生成したSTAスレッドへ転送"]
fwd -.-> hidden["隠しウィンドウへのメッセージとして届く"]
fwd --> nopump["メッセージポンプが回っていない"]
nopump --> norecv["STA側が呼び出しを受け取れない"]
norecv --> wait["呼び出し元は返事を待ち続ける"]
wait --> hang["ハング"]
図15: 転送はメッセージとして届くので、ポンプが止まっているSTAでは受け取り手がいなくなる。
なお、これは .NET でも同じです。Single-Threaded Apartments のドキュメントには、STAスレッドを Task.Wait()、Task.Result、Thread.Sleep()、ManualResetEvent.WaitOne() などでブロックすると、COMのコールバックやApartmentをまたぐ呼び出しが完了できずデッドロックになる、という警告が置かれています。次の 6.3 の失敗例が WaitOne() で止まっているのは、まさにこの形です。
一方、UIスレッドはウィンドウイベントを処理するために最初からメッセージループを回しているので、STAの要件を追加実装なしで満たしています。UIスレッドがSTAのCOMオブジェクトを動かす場所として自然な選択肢になるのは、このためです。
6.3. 擬似コード(典型的な失敗パターン)
using System;
using System.Runtime.InteropServices;
using System.Threading;
internal static class StaHangDemo
{
private const uint COINIT_APARTMENTTHREADED = 0x2;
[DllImport("ole32.dll")]
private static extern int CoInitializeEx(IntPtr pvReserved, uint dwCoInit);
[DllImport("ole32.dll")]
private static extern void CoUninitialize();
public static void Run(string progId)
{
var ready = new AutoResetEvent(false);
var done = new AutoResetEvent(false);
object comObj = null;
var staThread = new Thread(() =>
{
// STAとして初期化
CoInitializeEx(IntPtr.Zero, COINIT_APARTMENTTHREADED);
// ThreadingModel=Apartment で登録された COM クラスを想定
Type type = Type.GetTypeFromProgID(progId, throwOnError: true);
comObj = Activator.CreateInstance(type);
ready.Set();
// メッセージループがないまま待機 -> ここが致命傷
done.WaitOne();
CoUninitialize();
});
staThread.SetApartmentState(ApartmentState.STA);
staThread.Start();
ready.WaitOne();
try
{
// 別スレッド(STA/MTA問わず)から呼ぶと、呼び出しがSTAに転送される
// しかしSTA側はメッセージを処理しないため、ここでハングしやすい
dynamic obj = comObj;
obj.AnyMethod();
}
finally
{
// 呼び出しが返ってきた場合 ── agileなクラスだった、
// 呼び出しが転送されなかった、AnyMethodが即座に返った ── でも、
// 必ずSTAスレッドを解放する。ここを省くと、doneを待ち続ける
// フォアグラウンドスレッドが残り、「再現しなかった」ときも
// プロセスが終わらない。再現の有無を症状で見分けられなくなる
done.Set();
staThread.Join();
}
}
}
このコードは、ThreadingModel=Apartment(= STA)として登録された COM クラスの ProgID を渡す前提です。AnyMethod は、そのクラスが実際に持つメソッド名に置き換えてください。どの COM クラスでも再現するわけではありません。 再現に必要な条件は次の 3 つです。
| 条件 | 確認方法 |
|---|---|
対象クラスの ThreadingModel が Apartment |
レジストリの HKEY_CLASSES_ROOT\CLSID\{CLSID}\InprocServer32 の ThreadingModel 値を見ます。Both や Free だと呼び出しが転送されず、再現しません |
| STAスレッドがメッセージを回していない | 上の例の done.WaitOne() がそれにあたります |
| 呼び出しを 別のスレッド から行う | 同じSTAスレッド内から呼ぶ限り直接呼び出しになるので、ハングしません |
obj.AnyMethod() を try / finally で挟み、done.Set() と staThread.Join() を必ず通しているのは、この「再現しないケース」を見分けるためです。STAスレッドは既定でフォアグラウンドスレッドなので、done が立たない限りプロセスは終わりません。finally を省くと、再現したとき(呼び出しが返らない)と再現しなかったとき(呼び出しが返ったが done を誰も立てない)で、外から見える症状がどちらも「プロセスが終わらない」になります。再現条件を確かめるための道具が、条件の成否を隠してしまうわけです。上の形なら、再現しなかった場合はそのまま正常終了します。
止まったことの確認は、デバッガでプロセスを一時停止し、呼び出し元スレッドのスタックが COM の待機で止まっていること、STAスレッドが WaitOne で止まっていることを見るのが早いです。
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: 両方が待ち状態 → ハング
図16: WaitOneで止まったSTAスレッドと、転送の返事を待つメインスレッドが、互いに進めなくなる。
図の中央、「メッセージで転送するが…」の 2 行が、6.2 で書いた 2 つの前提が崩れている場所です。
6.4. 回避の要点
- 別スレッドからの呼び出しを受ける場合、STAスレッドはメッセージループを回す必要がある
- 可能ならUIスレッド上で生成・利用する(UIスレッドは最初からメッセージループがある)
- STAが不要なら最初からMTAにする
補足: 同一スレッド内だけで完結するなら、常に Application.Run() が必要とは限りません。
ただし、UI系・COM系は別スレッドからの呼び出しが絡むことが多いため、実務上はほぼ必須です。
flowchart TB
accTitle: ハング回避の3つの方向
accDescr: 別スレッドからの呼び出しを受けるならSTAスレッドでメッセージループを回す、可能なら最初からメッセージループを持つUIスレッド上で生成・利用する、STAが不要なら最初からMTAにする、という3つの回避の方向を示す図。
q["STAのハングをどう避けるか"] --> a1["STAスレッドでメッセージループを回す"]
q --> a2["UIスレッド上で生成・利用する"]
q --> a3["STAが不要なら最初からMTA"]
a2 -.-> why["UIスレッドは最初からループがある"]
図17: 回避策は、ループを回す・ループのある場所へ寄せる・STAをやめる、の3方向に整理できる。
6.5.「メッセージループを回す」って結局なに?
Win32のUIスレッドがやっている、例のこれです。
while (GetMessage(out var msg, IntPtr.Zero, 0, 0))
{
TranslateMessage(ref msg);
DispatchMessage(ref msg);
}
STAでは、別スレッドからの呼び出しが「転送」されてきます。 その転送を受け取って実行に回すのが、このループ(メッセージポンプ)だ、という話です。
flowchart TB
accTitle: メッセージポンプの役割
accDescr: 別スレッドから転送されてきた呼び出しを、GetMessageで受け取り、DispatchMessageで実行に回すという繰り返しがメッセージポンプであることを示す図。
fwd["転送されてきた呼び出し"] --> gm["GetMessageで受け取る"]
gm --> dm["DispatchMessageで実行に回す"]
dm --> gm
図18: 「メッセージループを回す」とは、転送を受け取って実行に回すこの繰り返しのこと。
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 の呼び忘れは普通に事故ります。CoInitializeEx の P/Invoke 宣言は 6.3 と同じものが要ります)
Application.Run() の引数なしの形は、フォームを持たずにメッセージループだけを回す ためのものです。用途としてはここがまさに想定される場面ですが、次の 2 点は押さえておいてください。
System.Windows.Formsへの参照が要ります。コンソールアプリなら、プロジェクトファイルに<UseWindowsForms>true</UseWindowsForms>を足します- このループは自分では終わりません。 止めるには、そのスレッド上で
Application.ExitThread()(またはアプリ全体を終えるApplication.Exit())を呼びます。上の例でCoUninitialize()まで到達させたいなら、STAスレッドへ「終了」を伝えてExitThreadを呼ばせる仕組みが別に必要です
WinForms に依存したくない場合は、6.5 の GetMessage / DispatchMessage ループを自分で書くか、メッセージと同期オブジェクトの両方を待てる MsgWaitForMultipleObjects を使います。後者は「イベントで終了通知を受けつつ、COM の呼び出しも取りこぼさない」形にしたいときの定番です。
flowchart TB
accTitle: バックグラウンドSTAでループを回す3つの手段
accDescr: バックグラウンドSTAでメッセージループを回すには、WinForms参照が要るApplication.Runを使う、GetMessageとDispatchMessageのループを自分で書く、メッセージと同期オブジェクトの両方を待てるMsgWaitForMultipleObjectsを使う、という3つの手段があることを示す図。
want["バックグラウンドSTAでループを回す"] --> ar["Application.Run"]
want --> gml["GetMessageループを自分で書く"]
want --> mw["MsgWaitForMultipleObjects"]
ar -.-> dep["WinForms参照と終了手段が必要"]
mw -.-> both["メッセージと同期オブジェクトの両方を待てる"]
図19: ループの回し方は3通りあり、終了のさせ方や依存の重さで選ぶことになる。
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: お互いが相手を待っている → デッドロック
図20: 同期呼び出しの戻りを待つUIスレッドと、コールバックの完了を待つサーバーが、互いを待ち合う。
なぜデッドロックになりやすいのか:
- UIスレッドが
DoWork()を同期呼び出し(ブロッキング) - UIスレッドは戻りを待っている(メッセージを処理していない)
- サーバーが
ProgressCallback()をUIスレッドに送る - UIスレッドは待機中なのでコールバックを受け取れない
- サーバーはコールバックの完了を待っている
- お互いが相手を待っている → 永遠に進まない
処理時間の長さは関係ありません。同期呼び出し中にコールバックが来るというパターン自体が問題になりやすいです。
補足: COMには状況によってメッセージを回す・再入する仕組みもあり、コンポーネントや呼び出し形態で挙動が変わります。 必ずデッドロックになるわけではありませんが、このパターンは避けるのが無難です。
7. ざっくり使い分け
| 状況 | 選ぶもの | 判断の理由 | あわせて必要なこと |
|---|---|---|---|
| UIが絡む(WinForms / WPF) | STA | UIコントロールが1スレッド親和性で、UIスレッドは元からメッセージループを持つため(3.1) | 特になし。既定でSTAです |
| 大量の並列処理をさせたい | MTA | 複数スレッドが1つのApartmentを共有し、転送なしで直接呼べるため(2.2) | COMオブジェクト側のスレッドセーフ設計。呼ぶ側の排他ではなく、オブジェクト実装側の責任です |
| バックグラウンドでSTAのCOMを使いたい | STA + メッセージループ | 別スレッドから呼ばれる以上、転送を受ける口が要るため(6.2) | Application.Run() か GetMessage ループ。終了手段(ExitThread など)も一緒に設計します(6.6) |
| 使うCOMコンポーネントの要求が決まっている | 相手に合わせる | Apartmentは呼び出し規則そのもので、後から変えられないため(5章) | ThreadingModel の値を確認します。Apartment ならSTA前提で組みます |
| 高頻度で呼び出す | 呼び出し側と同じApartmentへ寄せる | 境界を越えるたびにマーシャリングが挟まるため(2.4) | 難しければ、呼び出し回数自体を減らす(まとめて渡す)設計にします |
迷ったときの優先順位は、「相手の要求 > UIの有無 > 並列性」 の順で見ると決まりやすいです。Apartmentは最初の初期化で確定して後から変更できないので、ここだけは実装を始める前に決めておきます。
flowchart LR
accTitle: 迷ったときに見る順番
accDescr: STAとMTAの使い分けに迷ったときは、使うCOMコンポーネント側の要求、UIの有無、並列性の順に確認すると決まりやすいことを示す図。
p1["相手の要求"] -->|"次に"| p2["UIの有無"]
p2 -->|"次に"| p3["並列性"]
図21: 使い分けに迷ったら、相手の要求・UIの有無・並列性の順で確認する。
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
- Single-Threaded Apartments(メッセージループが必須である理由、隠しウィンドウ、.NETでのデッドロック警告) https://learn.microsoft.com/en-us/windows/win32/com/single-threaded-apartments
- Multithreaded Apartments https://learn.microsoft.com/en-us/windows/win32/com/multithreaded-apartments
- InprocServer32(
ThreadingModelの値) https://learn.microsoft.com/en-us/windows/win32/com/inprocserver32 - Application.Run メソッド(フォームなしでメッセージループを回す形と、その止め方) https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.application.run
- MsgWaitForMultipleObjects(メッセージと同期オブジェクトを同時に待つ) https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-msgwaitformultipleobjects
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
WinRTはCOMである ── IInspectable・.winmd・言語プロジェクション、そしてWinUIが今もバイナリ契約の上に乗っている理由
WinRTはマネージドランタイムではなく、COMにメタデータ(.winmd)と言語プロジェクションを足したABIです。IUnknownとIInspectableの関係から、デスクトップアプリでのHWND初期化やpackage identityの詰まりどころまで解説します。
OLEオブジェクトとは何か ── 埋め込み・リンクの仕組みと業務文書の落とし穴
WordにExcelの表を埋め込む機能の正体がOLEオブジェクトです。埋め込みとリンクの違い、複合ファイルと構造化ストレージ、In-Place Activationの仕組みから、リンク切れ・肥大化・セキュリティ対策まで実務目線で解説します。
クリップボードとドラッグ&ドロップの仕組み ── OLEデータ転送を業務アプリで正しく扱う
Excelの表を貼ると崩れる・コピー元を閉じると貼れない――正体は同じ内容を複数形式で置くクリップボードの仕組みです。標準形式・遅延レンダリング・OLEドラッグ&ドロップから履歴とクラウド同期のポリシーまで解説します。
Windowsシェル統合の今 ── 右クリックメニュー・ファイル関連付け・Windows 11の変化
Windows 11で右クリックメニューが「その他のオプションを確認」に隠れる理由を、拡張子→ProgID→verbという関連付けの基本、従来型シェル拡張の注意点、IExplorerCommandとMSIX/sparse packageの新方式まで整理して解説します。
Arm版Windowsで業務アプリは動くのか ── x64エミュレーション(Prism)とネイティブDLL・COMの現実
「Arm版Windowsで業務アプリは動くのか」に開発者・情シス向けに答えます。x64エミュレーション(Prism)の仕組み、ドライバーなど動かない層、.NETのAnyCPUとP/Invokeの問題、Arm対応チェックリストまで整理します。
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
ActiveX / 移行テーマ
COM / ActiveX / OCX を残すか、包むか、置き換えるかを整理するトピックです。
UI スレッド / タイマーテーマ
WPF / WinForms、UI スレッド、async/await、タイマー設計を整理するトピックです。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
技術相談・設計レビュー
STA / MTA、メッセージループ、マーシャリングの整理は、実装前の責務分割やスレッド境界レビューに直結します。
既存資産活用・移行支援
COM を含む既存資産を扱うときに避けにくい基礎なので、既存資産活用・移行支援 の相談とも相性がよいです。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- STAとMTAはどちらを選ぶべきですか?
- UIが絡む処理ならSTA、大量の並列処理ならMTAが基本の使い分けです。STAは1スレッドに1Apartmentという形でスレッド親和性が高い一方、呼び出し元が多いと渋滞しやすくなります。MTAは複数スレッドで1Apartmentを共有するため並列性が高いですが、COMオブジェクト側にスレッドセーフ設計が必須になります。どちらでもない場合は、使う既存ライブラリやCOMサーバーの要求に合わせるのが現実的です。
- なぜUIスレッドはSTAなのですか?
- UIスレッドとSTAは設計が一致しているためです。ボタンやテキストボックスなどのUIコントロールはスレッドセーフではなく、生成したスレッドからしか安全に操作できません。STAも同じく1スレッド親和性のモデルです。さらにUIスレッドはウィンドウイベント処理のために必ずメッセージループを回しており、STAの前提であるメッセージポンプを追加実装なしで満たします。このためWinForms/WPFのUIスレッドはデフォルトでSTAになっています。
- STAのCOMオブジェクトを呼ぶとハングするのはなぜですか?
- STAのCOMオブジェクトへの呼び出しは、生成したSTAスレッドで処理されます。別スレッドからの呼び出しはCOMがメッセージ/RPC経由で転送しますが、STAスレッドがメッセージループを回していないと転送を受け取れず、呼び出し元が待ち続けてハングします。回避するには、別スレッドから呼ばれるSTAスレッドでメッセージループを回すか、UIスレッド上で生成・利用するか、STAが不要なら最初からMTAにすることです。
- .NETの[STAThread]属性は何のためにありますか?
- COMのApartment Modelを設定するためのラッパーです。Mainメソッドに付けると、COMを使う際にそのスレッドがSTAとして初期化されます。ただし実際にCOMを呼ぶまでは初期化されず、COMを使わないアプリでは効果がありません。また追加で作るスレッドには効かないため、スレッド開始前にThread.SetApartmentStateで設定します。Apartmentは最初の初期化で決まり、後から変更できない点にも注意が必要です。