{"id":110965,"date":"2025-03-14T07:00:00","date_gmt":"2025-03-14T14:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/oldnewthing\/?p=110965"},"modified":"2025-03-14T08:54:14","modified_gmt":"2025-03-14T15:54:14","slug":"20250314-00","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/oldnewthing\/20250314-00\/?p=110965","title":{"rendered":"The case of COM failing to pump messages in a single-threaded COM apartment"},"content":{"rendered":"<p>A customer encountered a hang caused by COM not pumping messages while waiting for a cross-thread operation to complete. They were using <a title=\"Serializing asynchronous operations in C++\/WinRT\" href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20220915-00\/?p=107182\"> the <code>task_<wbr \/>sequencer<\/code> class<\/a> for serializing asynchronous operations on a UI thread they created to handle accessibility callbacks.<\/p>\n<p>The hang stack looked like this:<\/p>\n<pre>ntdll!ZwWaitForMultipleObjects+0x4\r\nKERNELBASE!WaitForMultipleObjectsEx+0xe0\r\ncombase!MTAThreadDispatchCrossApartmentCall+0x3a0\r\ncombase!CSyncClientCall::SendReceive2+0x65c\r\ncombase!DefaultSendReceive+0x88\r\ncombase!CSyncClientCall::SendReceive+0x390\r\ncombase!CClientChannel::SendReceive+0xc0\r\ncombase!NdrExtpProxySendReceive+0x68\r\nrpcrt4!NdrpClientCall3+0x764\r\ncombase!ObjectStublessClient+0x180\r\ncombase!ObjectStubless+0x34\r\ncombase!CObjectContext::InternalContextCallback+0x3f0\r\ncombase!CObjectContext::ContextCallback+0x80\r\ncontoso!winrt::impl::resume_apartment_sync+0x58\r\ncontoso!winrt::impl::resume_apartment+0xe8\r\ncontoso!winrt::impl::apartment_awaiter::await_suspend+0x6c\r\ncontoso!\u27e6lambda...\u27e7::operator()&lt;\u27e6...\u27e7&gt;+0x1c8\r\ncontoso!task_sequencer::chained_task::continue_with+0x38\r\ncontoso!task_sequencer::QueueTaskAsync&lt;\u27e6...\u27e7&gt;+0xd0\r\ncontoso!\u27e6lambda...\u27e7::&lt;lambda_invoker_cdecl&gt;+0xa0\r\nuser32!__ClientCallWinEventProc+0x34\r\nntdll!KiUserCallbackDispatcherReturn\r\nwin32u!ZwUserGetMessage+0x4\r\nuser32!GetMessageW+0x28\r\ncontoso!\u27e6lambda...\u27e7::operator()+0x204\r\ncontoso!std::thread::_Invoke&lt;\u27e6lambda...\u27e7&gt;+0x24\r\nucrtbase!thread_start&lt;unsigned int (__cdecl*)(void *),1&gt;+0x48\r\nkernel32!BaseThreadInitThunk+0x40\r\nntdll!RtlUserThreadStart+0x44\r\n<\/pre>\n<p>We see that we have a UI thread (notice the <code>Get\u00adMessage<\/code> at the bottom of the stack), yet COM decided to block without pumping messages (<code>Wait\u00adFor\u00adMultiple\u00adObjects\u00adEx<\/code> instead of (<code>Msg\u00adWait\u00adFor\u00adMultiple\u00adObjects\u00adEx<\/code>).<\/p>\n<p>Is this a bug in the task sequencer?<\/p>\n<p>Let&#8217;s look at the stack more closely. A message arrived via <code>__Client\u00adCall\u00adWin\u00adEvent\u00adProc<\/code>, and that then queued a task into the task sequencer. The <code>continue_<wbr \/>with<\/code> saw that the task sequencer had no active task, so it ran the new task immediately. That new task wants to run on a different thread, so C++\/WinRT&#8217;s apartment-switching code kicked in.<\/p>\n<p>The apartment-switching code went to resume_<wbr \/>apartment_<wbr \/>sync, which in turn called <a title=\"How do you get into a context via IContextCallback::ContextCallback?\" href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20191128-00\/?p=103157\"> our friend <code>IContext\u00adCallback::<wbr \/>Context\u00adCallback<\/code><\/a>, and that called into the COM thread-switching infrastructure, which doesn&#8217;t pump messages while wiating for the destination apartment to respond.<\/p>\n<p>Now, COM is a rather mature technology, and this code path is execised constantly throughout the system, so it&#8217;s unlikely that it simply &#8220;forgot&#8221; to pump messages. The function name <code>MTA\u00adThread\u00adDispatch\u00adCross\u00adApartment\u00adCall<\/code> strongly suggests that COM thinks that the thread is in an MTA. And the use of <code>resume_<wbr \/>apartment_<wbr \/>sync<\/code> suggests that <a href=\"https:\/\/github.com\/microsoft\/cppwinrt\/blob\/cf96df51cb808872c98301092b25e75de576c7d6\/strings\/base_coroutine_threadpool.h#L123,L131\"> C++\/WinRT also thinks that the thread is in an MTA<\/a>:<\/p>\n<pre>else if (<span style=\"border: solid 1px currentcolor;\">is_sta_thread()<\/span>)\r\n{\r\n    resume_apartment_on_threadpool(\r\n        context.m_context, handle, failure);\r\n    return true;\r\n}\r\nelse\r\n{\r\n    <span style=\"border: solid 1px currentcolor; border-bottom: none;\">return resume_apartment_sync(           <\/span>\r\n    <span style=\"border: solid 1px currentcolor; border-top: none;\">    context.m_context, handle, failure);<\/span>\r\n}\r\n<\/pre>\n<p>If this were an STA thread, then we would have called <code>resume_<wbr \/>apartment_<wbr \/>on_<wbr \/>threadpool<\/code> instead of <code>resume_<wbr \/>apartment_<wbr \/>sync<\/code>.<\/p>\n<p>Let&#8217;s take a closer look at this thread:<\/p>\n<pre>\/\/ Create a thread to receive accessibility notifications.\r\nm_thread = std::thread([this] {\r\n    ::SetThreadDescription(::GetCurrentThread(), L\"Accessibility STA\");\r\n\r\n    \u27e6 ... \u27e7\r\n\r\n    wil::unique_hwineventhook hook(SetWinEventHook(\u27e6...\u27e7));\r\n    THROW_LAST_ERROR_IF_NULL(hook);\r\n\r\n    MSG msg;\r\n    while (!m_stop &amp;&amp; GetMessage(&amp;msg, NULL, 0, 0)) {\r\n        TranslateMessage(&amp;msg);\r\n        DispatchMessage(&amp;msg);\r\n    }\r\n});\r\n<\/pre>\n<p>Ah, so there&#8217;s your problem.<\/p>\n<p>The thread claims to be an STA thread:<\/p>\n<pre>    ::SetThreadDescription(::GetCurrentThread(), L\"Accessibility <span style=\"border: solid 1px currentcolor;\">STA<\/span>\");\r\n<\/pre>\n<p>But there is nothing in the thread procedure that actually makes it an STA thread. It never initialized COM in single-threaded mode.<\/p>\n<p>The thread merely engaged in wishful thinking, proclaming itself to be an STA thread without actually becoming one. (Or maybe it believed in nominative determinism: The mere act of calling itself an STA thread was sufficient to make it true.)<\/p>\n<p>Since COM is already initialized elsewhere in the process, the new thread gets put into the implicit MTA by default, and it took no action to leave it, so from COM&#8217;s point of view, this thread is an MTA thread. And MTA threads are allowed to block without pumping messages.<\/p>\n<p>What they need to do is actually make it an STA thread, say, by calling <code>Co\u00adInitialize\u00adEx<\/code> with the <code>COINIT_<wbr \/>APARTMENT\u00adTHREADED<\/code> flag, and then uninitializing COM before the thread exits to return the thread to its original state. You can kill two birds with one stone with the help of the WIL RAII type.<\/p>\n<pre>\/\/ Create a thread to receive accessibility notifications.\r\nm_thread = std::thread([this] {\r\n    <span style=\"border: solid 1px currentcolor;\">auto uninit = wil::CoInitializeEx(COINIT_APARTMENTTHREADED);<\/span>\r\n\r\n    ::SetThreadDescription(::GetCurrentThread(), L\"Accessibility STA\");\r\n\r\n    \u27e6 ... \u27e7\r\n\r\n    wil::unique_hwineventhook hook(SetWinEventHook(\u27e6...\u27e7));\r\n    THROW_LAST_ERROR_IF_NULL(hook);\r\n\r\n    MSG msg;\r\n    while (!m_stop &amp;&amp; GetMessage(&amp;msg, NULL, 0, 0)) {\r\n        TranslateMessage(&amp;msg);\r\n        DispatchMessage(&amp;msg);\r\n    }\r\n});\r\n<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>A customer encountered a hang caused by COM not pumping messages while waiting for a cross-thread operation to complete. They were using the task_sequencer class for serializing asynchronous operations on a UI thread they created to handle accessibility callbacks. The hang stack looked like this: ntdll!ZwWaitForMultipleObjects+0x4 KERNELBASE!WaitForMultipleObjectsEx+0xe0 combase!MTAThreadDispatchCrossApartmentCall+0x3a0 combase!CSyncClientCall::SendReceive2+0x65c combase!DefaultSendReceive+0x88 combase!CSyncClientCall::SendReceive+0x390 combase!CClientChannel::SendReceive+0xc0 combase!NdrExtpProxySendReceive+0x68 rpcrt4!NdrpClientCall3+0x764 combase!ObjectStublessClient+0x180 [&hellip;]<\/p>\n","protected":false},"author":1069,"featured_media":111744,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[25],"class_list":["post-110965","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-oldnewthing","tag-code"],"acf":[],"blog_post_summary":"<p>A customer encountered a hang caused by COM not pumping messages while waiting for a cross-thread operation to complete. They were using the task_sequencer class for serializing asynchronous operations on a UI thread they created to handle accessibility callbacks. The hang stack looked like this: ntdll!ZwWaitForMultipleObjects+0x4 KERNELBASE!WaitForMultipleObjectsEx+0xe0 combase!MTAThreadDispatchCrossApartmentCall+0x3a0 combase!CSyncClientCall::SendReceive2+0x65c combase!DefaultSendReceive+0x88 combase!CSyncClientCall::SendReceive+0x390 combase!CClientChannel::SendReceive+0xc0 combase!NdrExtpProxySendReceive+0x68 rpcrt4!NdrpClientCall3+0x764 combase!ObjectStublessClient+0x180 [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/110965","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/users\/1069"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/comments?post=110965"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/110965\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media\/111744"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media?parent=110965"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/categories?post=110965"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/tags?post=110965"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}