为什么你说COM STA线程必须泵送消息,而我看到示例代码创建STA线程却没有泵送消息?

The Old New Thing (Raymond Chen) 新闻

摘要

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 账户上,讲一些不传达有用信息的故事。

相似文章

理解在试图绕过规则时规则背后的原理

The Old New Thing (Raymond Chen)

本文来自微软的《老东西》博客,解释了Windows内核回调函数最佳实践背后的原理,特别是为什么阻塞或等待工作项会违背其目的,并通过一个关于驱动程序导致系统挂起的警示故事来说明。

用户更改键盘布局时程序挂起的问题

The Old New Thing (Raymond Chen)

一个调试故事,讲述了当用户更改键盘布局(例如使用 Win+Space 快捷键)时,Windows 程序挂起的原因,是由于一个后台线程创建了窗口但没有泵送消息。修复方法是要么泵送消息,要么销毁窗口。

如何在低延迟的线程池上调度工作?

The Old New Thing (Raymond Chen)

本文来自《The Old New Thing》,解释了Windows线程池是为吞吐量而非延迟优化的,并提供了低延迟调度的解决方案,例如创建自定义线程池或使用专用工作线程,并附有C++和C#的代码示例。