当在STA线程上实例化COM对象时,线程通常必须实现消息泵,以便为来回调用其他线程(见here)。
可以手动提取消息,也可以依赖某些但不是全部的线程阻塞操作在等待时自动提取与COM相关的消息。文档通常无助于决定哪个是哪个(参见this related question)。
如何确定线程阻塞操作是否会在STA上泵送COM消息?
到目前为止的部分清单:
阻止执行泵*:
的操作Thread.Join
WaitHandle.WaitOne
/WaitAny
/WaitAll
(WaitAll
无法从STA线程调用)GC.WaitForPendingFinalizers
Monitor.Enter
(因此lock
) - 在某些情况下ReaderWriterLock
阻止不泵的操作:
Thread.Sleep
Console.ReadKey
(在某处阅读)*注意Noseratio's answer说即使是抽水的操作,也会针对一组非常有限的未公开的特定于COM的消息进行操作。
答案 0 :(得分:5)
BlockingCollection
确实会阻止。我已经了解到在回答以下问题时,其中有一些有关STA抽吸的有趣细节:
StaTaskScheduler and STA thread message pumping
然而,它将提供一组非常有限的未公开的COM特定消息,与您列出的其他API相同。它不会抽取通用的Win32消息(特殊情况是WM_TIMER
,也不会被调度)。对于某些需要全功能消息循环的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<Task> 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
,发布了handle.WaitOne
注册的常规Windows消息1}}。此消息不会被抽出并且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));
并改变原始代码:
await
现在时间间隔为~2000 ms,因为Application.DoEvents()
继续消息被WaitOneAndPump
抽出,任务完成并且其句柄已发出信号。
那就是说,我从不建议使用类似{{1}}的内容来生成代码(除了极少数特定情况)。它是UI重入等各种问题的根源。这些问题是微软将标准泵送行为仅限于特定COM特定消息的原因,这对于COM编组至关重要。
答案 1 :(得分:2)
实际上是如何披露泵送工作的。有对.NET运行时的内部调用,后者又使用CoWaitForMultipleHandles来执行STA线程上的等待。该API的文档非常缺乏,但阅读一些COM books和Wine source code可能会给您一些粗略的想法。
在内部,它使用QS_SENDMESSAGE |调用MsgWaitForMultipleObjectsEx QS_ALLPOSTMESSAGE | QS_PAINT标志。让我们剖析一下每个人的用途。
QS_PAINT是最明显的,WM_PAINT消息在消息泵中处理。 因此,在绘制处理程序中进行任何锁定是非常糟糕的,因为它可能会进入重入循环并导致堆栈溢出。
QS_SENDMESSAGE用于从其他线程和应用程序发送的消息。这实际上是进程间通信如何工作的一种方式。 丑陋的部分是它也用于来自资源管理器和任务管理器的UI消息,因此它会输出WM_CLOSE消息(右键单击任务栏中的无响应应用程序并选择关闭),托盘图标消息和可能的东西else(WM_ENDSESSION)。
QS_ALLPOSTMESSAGE用于其余部分。消息实际上已被过滤,因此只处理隐藏的公寓窗口和DDE消息(WM_DDE_FIRST - WM_DDE_LAST)的消息。
答案 2 :(得分:2)
我最近了解到Process.Start可能会采用的方法。我没有等待这个过程,也没有问过它的pid,我只是希望它能够并存。
在调用堆栈中(我手边没有)我看到它进入ShellInvoke特定的代码,所以这可能只适用于ShellInvoke = true。
虽然整个STA的抽水量足够令人惊讶,但至少可以说,我发现这一点非常令人惊讶!