为什么你说COM STA线程必须泵送消息,而我看到示例代码创建STA线程却没有泵送消息?
摘要
Raymond Chen解释说,COM STA线程仅在空闲时才需要泵送消息;一直忙碌的代码不需要显式的消息循环,但COM仍然会创建一个隐藏窗口,当线程变为空闲时需要泵送消息以避免阻塞窗口广播。
<p>COM单线程单元(STA)的规则之一是,该单元中的线程必须泵送消息。但我们也看到一些代码以单线程模式初始化COM,却从不泵送消息。考虑来自<a href="https://github.com/microsoft/Windows-classic-samples/blob/adf93f04125e0b5bd84ad0a9a073f7fea993a360/Samples/MSXML/DynamDom/cpp/dynamDOM.cpp#L271C1-L280C2">这个函数</a>的<a href="https://github.com/microsoft/Windows-classic-samples/blob/adf93f04125e0b5bd84ad0a9a073f7fea993a360/Samples/MSXML/DynamDom/README.md">XML DOM对象动态创建示例</a>:</p>
<pre>int __cdecl wmain()
{
HRESULT hr = CoInitialize(NULL);
if (SUCCEEDED(hr))
{
dynamDOM();
CoUninitialize();
}
return 0;
}
</pre>
<p><code>CoInitialize</code>函数以单线程单元模式初始化COM,然后程序执行一些工作,然后取消初始化COM,并且<i>从未泵送消息</i>。这是怎么回事?难道不应该有一个消息循环吗?</p>
<p>关于单线程单元的规则是,它们必须在<i>空闲时</i>泵送消息。如果它们正忙于做某事,那么显然它们无法泵送消息,因为它们正忙着做别的事情!¹</p>
<p>如果你的线程将COM初始化为单线程单元,然后执行大量工作,然后取消初始化COM,那么这没问题。你的线程从未空闲,所以它没有机会泵送消息。(不过,如果你的线程向其他线程发出了COM调用,COM在等待回复时会泵送消息,因此在线程空闲时它确实泵送了消息。)</p>
<p>在空闲时不泵送消息意味着当另一个线程想要与你的线程通信时,它永远不会得到响应。如果你的线程很忙,那么其他线程没有得到你的响应也没问题——毕竟你正忙着做别的事情。但是,如果你处于单线程COM单元中,并且已经完成正在做的事情,你需要泵送消息以查看COM是否有任何工作要你做,或者你需要取消初始化COM。</p>
<p>现在,你可能会说:“你看,我的线程没有创建任何窗口,也没有做任何跨线程COM的事情,所以谁在乎它没有泵送消息呢?又不会有人要求这个线程做任何事情,而且由于它没有创建窗口,没人能给它发送任何东西。”</p>
<p>啊哈,但你看,你的线程<i>确实</i>创建了一个窗口。当你将线程初始化为单线程单元时,<u>COM</u>会创建一个窗口。它创建这个窗口是为了接收让该线程做某事的入站请求。如果你不泵送消息,那么你有一个线程被阻塞而不泵送消息,这将阻塞窗口广播。</p>
<p>¹ 对“STA线程必须泵送消息”这一规则的一种故意曲解是,你的线程除了调用<code>GetMessage</code>和<code>DispatchMessage</code>之外不能做任何事情!因为任何其他代码行都不是“泵送消息”。</p>
<p>这篇帖子<a href="https://devblogs.microsoft.com/oldnewthing/20260522-00/?p=112348">为什么你说COM STA线程必须泵送消息,而我看到示例代码创建STA线程却没有泵送消息?</a>首次出现在<a href="https://devblogs.microsoft.com/oldnewthing">The Old New Thing</a>上。</p>
查看缓存全文
缓存时间: 2026/05/23 08:45
# 为什么说COM STA线程必须泵消息,而示例代码中创建的STA线程却没有泵消息? - The Old New Thing
来源:https://devblogs.microsoft.com/oldnewthing/20260522-00?p=112348
COM单线程单元(STA)的规则之一是:该单元中的线程必须泵消息。但我们也能看到一些代码,它们以单线程模式初始化COM,却从未泵消息。考虑这个来自 XML DOM 对象动态创建示例 (https://github.com/microsoft/Windows-classic-samples/blob/adf93f04125e0b5bd84ad0a9a073f7fea993a360/Samples/MSXML/DynamDom/cpp/dynamDOM.cpp#L271C1-L280C2) 的函数 (https://github.com/microsoft/Windows-classic-samples/blob/adf93f04125e0b5bd84ad0a9a073f7fea993a360/Samples/MSXML/DynamDom/README.md):
```
int __cdecl wmain()
{
HRESULT hr = CoInitialize(NULL);
if (SUCCEEDED(hr))
{
dynamDOM();
CoUninitialize();
}
return 0;
}
```
`CoInitialize` 函数以单线程单元模式初始化COM,然后程序执行一些工作,接着卸载COM —— 并且**从未泵消息**。这是怎么回事?难道不应该有一个消息循环吗?
关于单线程单元的规则是:它们必须在**空闲时**泵消息。如果它们正忙着做某事,显然无法泵消息,因为它们正忙着呢!¹
如果你的线程以单线程单元初始化COM,然后做了一堆工作,再卸载COM,那完全没问题。你的线程从未空闲,因此它没有机会泵消息。(但注意:如果你的线程向其他线程发出了COM调用,那么在等待回复期间,COM会泵消息 —— 所以实际上当线程空闲时它确实泵了消息。)
空闲时不泵消息意味着:当其他线程想与你的线程通信时,它永远得不到响应。如果你的线程正忙,那么其他线程得不到响应也没关系 —— 毕竟你正忙于其他事情。但是,如果你处于单线程COM单元中,并且已经完成了手头的工作,那么你需要泵消息,看看是否有COM要你做的活,或者你需要卸载COM。
现在你可能会说:“你看,我的线程没有创建任何窗口,也没有做任何跨线程的COM操作,所以它不泵消息又怎样?反正不会有人让这个线程做任何事,而且既然没创建窗口,也没人能给它发送任何东西。”
啊哈,但你瞧,你的线程**确实**创建了一个窗口。当你将线程初始化为单线程单元时,COM会创建一个窗口。它创建这个窗口是为了能够接收让该线程执行任务的入站请求。如果你不泵消息,那么线程就会阻塞而不泵消息,进而阻塞窗口广播。
¹ 对“STA线程必须泵消息”这一规则的一种故意钻牛角尖的解释是:你的线程除了调用 `GetMessage` 和 `DispatchMessage` 之外什么都不能做!因为任何其他代码行都不属于“泵消息”。
### 分类
### 主题
## 作者
Raymond Chen
Raymond 参与 Windows 的发展已有三十多年。2003年,他创办了一个名为 The Old New Thing 的网站,其受欢迎程度远远超出了他最疯狂的想象,这一发展至今仍让他感到不安。该网站后来催生了一本书,巧合的是书名也叫 The Old New Thing(Addison Wesley 2007)。他偶尔会出现在 Windows Dev Docs 的 Twitter 账户上,讲一些不传达有用信息的故事。
相似文章
理解在试图绕过规则时规则背后的原理
本文来自微软的《老东西》博客,解释了Windows内核回调函数最佳实践背后的原理,特别是为什么阻塞或等待工作项会违背其目的,并通过一个关于驱动程序导致系统挂起的警示故事来说明。
用户更改键盘布局时程序挂起的问题
一个调试故事,讲述了当用户更改键盘布局(例如使用 Win+Space 快捷键)时,Windows 程序挂起的原因,是由于一个后台线程创建了窗口但没有泵送消息。修复方法是要么泵送消息,要么销毁窗口。
如何在低延迟的线程池上调度工作?
本文来自《The Old New Thing》,解释了Windows线程池是为吞吐量而非延迟优化的,并提供了低延迟调度的解决方案,例如创建自定义线程池或使用专用工作线程,并附有C++和C#的代码示例。
如果C#和JavaScript允许我多次等待Windows Runtime异步操作,为什么C++/WinRT不行?
Raymond Chen解释了为什么C++/WinRT不像C#、JavaScript和Python那样允许多次等待异步操作,其原因是没有标准库的task类型,以及不为你未使用的功能付费的原则。
@petergyang: 有时候我给 Claude Code 发消息时,它会卡住 3 分钟,我根本不知道它是否还在运行。……
有用户反映,Claude Code 有时会卡住三分钟,且不提供任何状态更新或反馈,让人无法判断它是否仍在处理。