COM STA/MTA 基礎 - 執行緒模型與避免 Hang 的思考方式
· 小村 豪 · COM, Windows 開發, STA, MTA, 執行緒
COM STA/MTA 基礎 - 執行緒模型與避免 Hang 的思考方式
COM 的 STA/MTA,在從 Windows 或 .NET 碰 COM 時是難以避開的基礎知識。
特別常在搜尋中出現的問題是:UI 執行緒為什麼是 STA、跨 Apartment 會發生什麼事、為什麼會 Hang。
目次
- 先說結論(一句話)
- Apartment 模型的呼叫模式(圖)
- STA(Single-Threaded Apartment)
- MTA(Multi-Threaded Apartment)
- STA/MTA 在哪裡決定
- STA 處理錯誤時會發生的 Hang 具體例
- 大致的使用分類
- 總結
- 參考資料
使用 COM 時,「在哪一個執行緒上跑」 是無法迴避的議題。
核心就是 Apartment 模型(STA/MTA)。
STA/MTA 是 COM 專用的執行緒模型。
它不是 Windows 一般的執行緒概念,而是決定 COM 物件呼叫規則的機制。
本文會用 圖來整理 STA、MTA 與 COM 的關係,並接續說明 「為什麼會 Hang」。
1. 先說結論(一句話)
- COM 物件以「屬於哪個 Apartment」來決定呼叫規則
- STA 是 1 執行緒 = 1 Apartment,MTA 是 多執行緒 = 1 Apartment 這樣理解最輕鬆
- 跨 Apartment 的呼叫,COM 會經由 Proxy/Stub 進行 Marshaling
2. Apartment 模型的呼叫模式(圖)
COM 物件的呼叫大致可分為三種模式。
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(一個 Apartment)]
Thread1[工作執行緒 1]
Thread2[工作執行緒 2]
Obj[COM 物件]
Thread1 -->|直接呼叫| Obj
Thread2 -->|直接呼叫| Obj
end
2.3. 模式3:跨 Apartment 的呼叫
在不同 Apartment 之間 COM 會用 Proxy/Stub 轉送。
若是標準介面,COM Runtime 會自動處理。
注意: Proxy/Stub 並不是「什麼都會自動生成」。
只是實務上大多數情況不需要明確生成。
| 模式 | Proxy/Stub 準備 |
|---|---|
IDispatch 基底(Automation) |
不需要。由 oleaut32.dll 處理 |
| 已註冊的型別程式庫 | 不需要。由 Type Library Marshaler 處理 |
| .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 Runtime(自動)]
Proxy[Proxy]
RPC[RPC/IPC]
Stub[Stub]
Proxy --> RPC --> Stub
end
subgraph MTA[MTA 執行緒]
MtaObj[COM 物件]
end
StaCaller -->|呼叫| Proxy
Stub -->|轉送| MtaObj
重點:
跨 Apartment 會產生 Marshaling 的額外負擔。
高頻率呼叫時會影響效能,設計時必須考量。
2.4. Marshaling 的負擔大致估計
以下是一般參考值(非實測,會依情境與參數複雜度有很大差異)。
| 呼叫模式 | 大約時間 | 相對感覺 |
|---|---|---|
| 同一 Apartment 內(直接) | 10~100 奈秒 | 和一般函式呼叫差不多 |
| 不同 Apartment(同一程序內) | 1~10 微秒 | 直接呼叫的 100~1000 倍 |
| 不同程序(Out-of-proc) | 100~1000 微秒 | 直接呼叫的 1 萬~10 萬倍 |
相對比較:
- 同一 Apartment:約一次記憶體存取
- 不同 Apartment:約一次系統呼叫
- 不同程序:約一次 localhost 的網路通訊
在迴圈中呼叫一萬次的場景,這個差距會非常明顯。
3. STA(Single-Threaded Apartment)
STA 是 「1 執行緒 = 1 Apartment」 的模型。
- 該 Apartment 內的 COM 物件 原則上只在那個執行緒上執行
- 其他執行緒呼叫時,COM 會經由訊息佇列/RPC 轉送
- 常用於 UI 執行緒(WinForms/WPF)(UI 也是「1 執行緒親和性+訊息迴圈」,所以很搭)
3.1. 為什麼 UI 執行緒用 STA
因為 UI 執行緒和 STA 設計一致。
- UI 控制項不是執行緒安全的
Button、TextBox 只能由建立它的執行緒安全操作 - STA 同樣具有「1 執行緒親和性」
COM 物件只在建立它的執行緒上直接執行 - UI 執行緒必定有訊息迴圈
為了處理視窗事件必須有,和 STA 的前提(訊息幫浦)一致
因此 WinForms/WPF 的 UI 執行緒 預設就是 STA。
重點:
STA 執行緒親和性高,相對地 呼叫者多的時候容易塞車。
4. MTA(Multi-Threaded Apartment)
MTA 是 「多執行緒 = 1 Apartment」 的模型。
- 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 模型的包裝。
[STAThread]→ 標註在 Main 方法(進入點)。使用 COM 時會以 STA 初始化[MTAThread]→ 同樣用於 Main 方法。會以 MTA 初始化Thread.SetApartmentState(ApartmentState.STA)→ 用於額外建立的執行緒。必須在執行緒啟動前設定
注意:
- 即便有
[STAThread],在實際呼叫 COM 之前並不會初始化(不用 COM 就沒效果) - 額外的執行緒
[STAThread]無效。要用Thread.SetApartmentState
也就是說,.NET 的 STA/MTA 就是 COM 的 STA/MTA 本身。
它不是 .NET 自己的執行緒模型,而是為了 COM Interop 而準備的機制。
重要:
初始化之後不能再改 Apartment。第一次的初始化就是全部。
6. STA 處理錯誤時會發生的 Hang 具體例
以下這種配置 實際上很容易 Hang。
6.1. 常見情境
- 在背景建立一個 STA 執行緒並生成 COM 物件
- 該執行緒 沒有在跑訊息迴圈
- 從其他執行緒(不論 STA/MTA)呼叫那個 COM 物件
6.2. 到底發生了什麼
STA 的 COM 物件 必須在那個 STA 執行緒上處理呼叫。
無論呼叫方是 STA 還是 MTA,只要是其他執行緒,COM 都會用訊息/RPC 轉送。
但 STA 執行緒 不處理訊息 的情況下,呼叫會一直等下去,結果就是 Hang。
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 那邊不處理訊息,所以這裡很容易 Hang
CallComObject(comObj);
sequenceDiagram
participant Main as 主執行緒
participant STA as STA 執行緒
participant COM as COM Runtime
Main->>STA: 執行緒啟動
STA->>STA: CoInitializeEx(STA)
STA->>STA: 生成 COM 物件
STA->>Main: ready.Set()
STA->>STA: done.WaitOne() 等待
Note over STA: 沒有訊息迴圈
卡在這裡
Main->>COM: CallComObject()
COM->>STA: 試圖轉送呼叫
Note over COM: 用訊息轉送,但...
Note over STA: 正在 WaitOne
無法處理訊息
Note over Main: 呼叫方也繼續等
Note over Main,STA: 雙方都在等 → Hang
簡言之:
這裡說的「前提」,是為了解釋 「為什麼在 STA 跨執行緒呼叫會 Hang」 而提到的前提。
STA 的前提有 兩點:
- COM 物件在建立它的 STA 執行緒上處理
其他執行緒的呼叫一定會被轉送到那個 STA 執行緒 - 為了接收那些轉送,STA 執行緒必須跑訊息幫浦
沒跑就接不到
所以,
- 沒跑訊息的 STA 執行緒 接不到呼叫
- 接不到的結果就是呼叫方一直等,最後 Hang
另一方面,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. 另一個 Hang 例子:同步呼叫中的 Callback
STA 除了「呼叫會被轉送過來」之外,某些情境下還會有 反方向(伺服器 → 客戶端)的 Callback。
同步呼叫中發生 Callback 的模式 很容易造成死結。
sequenceDiagram
participant UI as UI 執行緒(STA)
participant Server as COM 伺服器
UI->>Server: DoWork()(同步呼叫)
Note over UI: 等待 DoWork 回傳
(不處理訊息)
Server->>UI: ProgressCallback()(回呼)
Note over UI: 等待中
收不到回呼
Note over Server: 等回呼完成
Note over UI,Server: 彼此在等對方 → 死結
為什麼容易死結:
- UI 執行緒 同步呼叫(阻塞)
DoWork() - UI 執行緒在等回傳(不處理訊息)
- 伺服器對 UI 執行緒送
ProgressCallback() - UI 執行緒等待中,無法接收回呼
- 伺服器在等回呼完成
- 彼此在等對方 → 永遠不前進
處理時間的長短無關。同步呼叫中來回呼 的模式本身就容易出問題。
補充: COM 在某些情境下也會轉訊息、再入,依元件與呼叫形態行為不同。
不一定會死結,但這種模式最好避免。
7. 大致的使用分類
- UI 相關 → STA
- 大量並行處理 → MTA
- 都不算 → 依現有函式庫或 COM 伺服器的需求決定
8. 總結
STA/MTA 是什麼:
- STA/MTA 是 COM 專用的執行緒模型(不是 Windows 一般的執行緒概念)
- STA 是 1 執行緒 = 1 Apartment,MTA 是 多執行緒 = 1 Apartment
- 跨 Apartment 時 COM 會透過 Proxy/Stub 轉送(標準介面以外要用 MIDL 等生成並註冊)
STA 的前提與陷阱:
- 要接收其他執行緒的呼叫時,STA 必須跑訊息幫浦
- 沒跑訊息的 STA 執行緒收到呼叫 很容易 Hang
- 同步呼叫中來回呼 的模式 很容易死結
UI 執行緒與 STA 的關係:
- UI 執行緒本來就有「1 執行緒親和性」與「訊息迴圈」
- 所以不需要額外實作就滿足 STA 的條件,和 STA COM 很搭
設計時要注意:
- 跨 Apartment 的呼叫有 Marshaling 的額外負擔
- 高頻率呼叫時會影響效能,Apartment 設計必須謹慎
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
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
開發 COM 元件、OCX/ActiveX 時常見的坑 - 整理 Visual Studio 的 32bit/64bit、註冊、管理員權限
整理開發 COM、OCX、ActiveX 元件時最容易卡關的四個面向:宿主行程的 32bit/64bit、Visual Studio 2022 變成 64bit 後的設計時整合、regsvr32 與 Regasm 的註冊位置、以及管理員權限與 HKCU/HKLM 的關係,協...
什麼是 Reg-Free COM - 免註冊使用 COM 的機制,以及合用與不合用的情境
整理 Reg-Free COM 的本質、執行時 activation context 與 manifest 的協作方式,以及好處與極限。同時釐清 bitness、相依 DLL、TLB/設計時參考是另一條線,幫助你判斷哪些情境適合導入、哪些情境得另想辦法,並避開常見的部署陷阱。
Excel 報表輸出該怎麼做 - COM 自動化 / Open XML / 範本方式的判斷表
從 Windows 應用與業務系統的角度,把 Excel 報表輸出拆成驅動 Excel 與組裝 Excel 檔案兩條路。整理 COM 自動化、Open XML 直接生成、範本套版、既有 VBA 併用的取捨,並針對使用者編輯、夜間批次、大量輸出等情境,給出不易壞且容易維運的選...
COM / ActiveX / OCX 是什麼 - 差異與關係一次整理
從實務角度釐清 COM、ActiveX、OCX 三者的差異與關係:COM 是 Windows 元件互動的 binary 契約底層,ActiveX 是以 COM 為基礎的可嵌入 control 脈絡,OCX 則為 ActiveX control 常見的副檔名。讀完能分清機制、...
ActiveX / OCX 現在如何處理 - 保留・包裝・取代的判斷表
整理在實務專案中遇到 ActiveX 或 OCX 時的判斷流程,從 UI 部件、機器控制、報表、瀏覽器依賴到 32bit 與 64bit 的牆壁,依照保留・包裝・取代三種選項列出對照表與決策流程,並說明註冊發佈與 STA 等容易絆倒的細節,幫讀者選出最低成本的下一步。
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
ActiveX 遷移
整理保留、包裝或替換 COM / ActiveX / OCX 資產的階段性判斷的主題頁面。
UI 執行緒 & 計時器
整理 WPF / WinForms UI 執行緒、非同步流程、Dispatcher 使用、計時器判斷的主題頁面。
與本主題相關的服務
本文連結到以下服務頁面,歡迎從最接近的入口查看。
Windows 應用程式開發
支援包含常駐處理、設備連動、運作日誌與可維護結構的 Windows 桌面應用程式。
既有資產活用 & 遷移支援
在持續活用 COM / ActiveX / OCX 資產、原生程式碼與 32 位元相依的同時,協助規劃階段性的遷移。
作者檔案
本文作者的個人檔案頁面。
Go Komura
小村軟體有限公司 代表
以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。