COM 為了解決多執行緒存取同一物件的同步問題,定義了 **Apartment** 模型:
在類別庫中若需使用 **ActiveX 控制項、Clipboard、File Dialog、COM 元件** 等強制要求 STA 的功能,必須在 STA 執行緒上執行。
// Program.cs
[STAThread] // 標記 Main 進入點為 STA
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
}
`[STAThread]` 屬性指示 CLR 以 `CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)` 初始化主執行緒。
原文提供的 `CLK.Threading.STAThread` 是一個**訊息泵式工作佇列**,允許從任意執行緒 (MTA/ThreadPool) **Post/Send** 委派到專屬 STA 執行緒執行。
// 簡化版關鍵結構
public class StaTaskScheduler : TaskScheduler, IDisposable
{
private readonly Thread _staThread;
private readonly BlockingCollection _queue = new BlockingCollection();
public StaTaskScheduler()
{
_staThread = new Thread(() =>
{
// 關鍵:設定為 STA
Thread.CurrentThread.SetApartmentState(ApartmentState.STA);
// 啟動訊息泵 (WinForms 需 Application.Run,Console 需自建迴圈)
Application.Run(new ApplicationContext()); // 或自訂迴圈
})
{ IsBackground = false, Name = "STA Worker" };
_staThread.Start();
}
protected override void QueueTask(Task task) => _queue.Add(task);
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) => false;
protected override IEnumerable GetScheduledTasks() => _queue.ToArray();
public void Dispose()
{
_queue.Add(null); // 毒丸結束迴圈
_staThread.Join();
}
}
// 共享單一 STA 排程器 (單例)
static readonly StaTaskScheduler StaScheduler = new StaTaskScheduler();
// 在 MTA/ThreadPool 執行緒中安全呼叫 COM/Clipboard
public async Task GetClipboardTextAsync()
{
return await Task.Factory.StartNew(() =>
{
return Clipboard.GetText(); // 必須在 STA
}, CancellationToken.None, TaskCreationOptions.None, StaScheduler);
}
// 同步版本 (謹慎使用,易死結)
public string GetClipboardText()
{
var t = Task.Factory.StartNew(() => Clipboard.GetText(), StaScheduler);
t.Wait(); // 若呼叫端本身在 STA 會死結!
return t.Result;
}
| 特性 | 實作細節 |
|---|---|
| 狀態機管理 | `ThreadState` (Started/Stopping/Stopped) + `lock(_syncRoot)` 保護狀態轉換 |
| 工作佇列 | `BlockingQueue |
| Post (非同步) | 封裝 `SendOrPostCallback` 入佇列,立即返回,不等待執行完成 |
| Send (同步) | 建立 `ManualResetEvent` 等待完成,支援跨執行緒同步調用,遞迴呼叫檢測避免死結 |
| 例外保護 | 委派執行包裝 `try-catch`,`Debug.Fail` 記錄不中斷訊息泵 |
| 優雅關閉 | `Stop()` 發送毒丸 `null` Action,觸發迴圈結束,`_threadEvent.WaitOne()` 等待執行緒結束 |
| 陷阱 | 現象 | 解決 |
|---|---|---|
| 在 STA 執行緒同步等待另一 STA 工作 | 死結 (訊息泵被阻塞無法處理內送呼叫) | 全面改用 `async/await` + `Post`,或使用 `Task.Run` 切換到 ThreadPool 再等待 |
| Clipboard 在非 STA 呼叫 | 拋出 `ThreadStateException` | 務必透過 STA 排程器執行 |
| COM 物件跨 Apartment 傳遞 | 效能差 (Proxy/Stub 封送) | 在建立 COM 物件的 STA 執行緒上完成所有操作,僅傳遞基礎型別結果 |
| WPF 應用中建立第二個 STA | 需自行 Dispatcher.Run() | 使用 `DispatcherFrame` 或獨立 `Application` 實例 |
TaskScheduler.FromCurrentSynchronizationContext():在 UI 執行緒取得排程器,直接 `await` 切回 UI。Microsoft.VisualBasic.Devices.Computer 的 `Clipboard` 內部已處理 STA 切換 (但效能較差)。理解 STA 本質是「單一執行緒 + 訊息泵」,類別庫需求時應**封裝專用 STA 工作佇列**,暴露非同步 `Post`/`Send` 介面,避免呼叫端阻塞。原文 `CLK.Threading.STAThread` 展示了完整的狀態管理、佇列同步、優雅關閉模式,是建立高可用 STA 元件的優秀參考實作。