哪些阻塞操作导致STA线程泵送COM消息?

当一个COM对象在STA线程上实例化时,该线程通常必须实现一个消息泵,以便为来回调用其他线程(见此处 )。

可以手动泵送消息,或者依赖于某些( 但不是全部 )线程阻塞操作在等待时自动泵送COM相关消息的事实。 文档通常无助于决定哪个是哪个(参见相关问题 )。

如何确定线程阻塞操作是否会在STA上泵送COM消息?

到目前为止的部分清单:

阻塞操作泵*:

  • Thread.Join
  • WaitHandle.WaitOne / WaitAny / WaitAll (虽然无法从STA线程调用WaitAll
  • GC.WaitForPendingFinalizers
  • Monitor.Enter (因此lock ) – 在某些条件下
  • ReaderWriterLock
  • BlockingCollection

阻止泵送的操作:

  • Thread.Sleep
  • Console.ReadKey (在某处读取)

*注意Noseratio的答案说,即使是操作泵,也是为了非常有限的未公开的COM特定消息集。

BlockingCollection确实会阻塞。 我已经了解到在回答以下问题时,其中有一些有关STA抽吸的有趣细节:

StaTaskScheduler和STA线程消息泵送

但是, 它将提供一组非常有限的未公开的COM特定消息 ,与您列出的其他API相同。 它不会抽取通用的Win32消息(特殊情况是WM_TIMER ,也不会被调度)。 对于某些需要全function消息循环的STA COM对象,这可能是一个问题。

如果您想试验这一点,请创建自己的SynchronizationContext版本,覆盖SynchronizationContext.Wait ,调用SetWaitNotificationRequired并在STA线程上安装自定义同步上下文对象。 然后在Wait设置一个断点,看看会调用哪些API。

WaitOne的标准泵送行为在多大程度上受到限制? 下面是导致UI线程死锁的典型示例。 我在这里使用WinForms,但同样的问题适用于WPF:

 public partial class MainForm : Form { public MainForm() { InitializeComponent(); this.Load += (s, e) => { Func doAsync = async () => { await Task.Delay(2000); }; var task = doAsync(); var handle = ((IAsyncResult)task).AsyncWaitHandle; var startTick = Environment.TickCount; handle.WaitOne(4000); MessageBox.Show("Lapse: " + (Environment.TickCount - startTick)); }; } } 

消息框将显示~4000 ms的时间间隔,但任务只需2000 ms即可完成。

发生这种情况是因为await继续回调是通过WindowsFormsSynchronizationContext.Post安排的,后者使用Control.BeginInvoke ,后者又使用PostMessage ,发布一个使用RegisterWindowMessage的常规Windows消息。 此消息不会被泵送和处理handle.WaitOne超时。

如果我们使用handle.WaitOne(Timeout.Infinite) ,我们就会遇到经典的死锁。

现在让我们实现一个带有显式抽取的WaitOne版本(并将其WaitOneAndPump ):

 public static bool WaitOneAndPump( this WaitHandle handle, int millisecondsTimeout) { var startTick = Environment.TickCount; var handles = new[] { handle.SafeWaitHandle.DangerousGetHandle() }; while (true) { // wait for the handle or a message var timeout = (uint)(Timeout.Infinite == millisecondsTimeout ? Timeout.Infinite : Math.Max(0, millisecondsTimeout + startTick - Environment.TickCount)); var result = MsgWaitForMultipleObjectsEx( 1, handles, timeout, QS_ALLINPUT, MWMO_INPUTAVAILABLE); if (result == WAIT_OBJECT_0) return true; // handle signalled else if (result == WAIT_TIMEOUT) return false; // timed-out else if (result == WAIT_ABANDONED_0) throw new AbandonedMutexException(-1, handle); else if (result != WAIT_OBJECT_0 + 1) throw new InvalidOperationException(); else { // a message is pending if (timeout == 0) return false; // timed-out else { // do the pumping Application.DoEvents(); // no more messages, raise Idle event Application.RaiseIdle(EventArgs.Empty); } } } } 

并像这样更改原始代码:

 var startTick = Environment.TickCount; handle.WaitOneAndPump(4000); MessageBox.Show("Lapse: " + (Environment.TickCount - startTick)); 

现在时间间隔将是~2000毫秒,因为await继续消息被Application.DoEvents()抽取,任务完成并且其句柄被发出信号。

也就是说, 我从不建议使用像WaitOneAndPump的产品代码 (除了极少数特定情况)。 它是UI重入等各种问题的根源。 这些问题是微软将标准泵送行为仅限于特定COM特定消息的原因,这对于COM编组至关重要。

实际上披露了泵送的工作原理。 有对.NET运行时的内部调用,后者又使用CoWaitForMultipleHandles来执行STA线程上的等待。 该API的文档非常缺乏,但阅读一些COM书籍和Wine源代码可能会给你一些粗略的想法。

在内部,它使用QS_SENDMESSAGE |调用MsgWaitForMultipleObjectsEx QS_ALLPOSTMESSAGE | QS_PAINT标志。 让我们剖析一下每个人的用途。

QS_PAINT是最明显的,WM_PAINT消息在消息泵中处理。 因此,在绘制处理程序中进行任何锁定是非常糟糕的,因为它可能会进入重入循环并导致堆栈溢出。

QS_SENDMESSAGE用于从其他线程和应用程序发送的消息。 这实际上是进程间通信如何工作的一种方式。 丑陋的部分是它还用于来自资源管理器和任务管理器的UI消息,因此它会输出WM_CLOSE消息(右键单击任务栏中的无响应应用程序并选择关闭),托盘图标消息以及可能的其他内容(WM_ENDSESSION) )。

QS_ALLPOSTMESSAGE用于其余部分。 消息实际上已过滤,因此仅处理隐藏的公寓窗口和DDE消息(WM_DDE_FIRST – WM_DDE_LAST)的消息。

我最近了解到Process.Start可能会遇到的困难。 我没有等待这个过程,也没有问它的pid,我只是希望它能够并行运行。

在调用堆栈中(我没有手头)我看到它进入ShellInvoke特定的代码,所以这可能只适用于ShellInvoke = true。

虽然整个STA的抽水量足够令人惊讶,但我发现这一点非常令人惊讶,至少可以说!