问题描述
当在 STA 线程上实例化 COM 对象时,该线程通常必须实现消息泵,以便编组对其他线程的调用(请参阅 这里).
When a COM object is instantiated on an STA thread, the thread usually has to implement a message pump in order to marshal calls to and fro other threads (see here).
可以手动泵送消息,也可以依靠某些但不是全部线程阻塞操作会在等待时自动泵送与 COM 相关的消息这一事实.文档通常无助于确定哪个是哪个(请参阅此相关问题).
One can either pump messages manually, or rely on the fact that some, but not all, thread-blocking operations will automatically pump COM-related messages while waiting. The documentation often doesn't help in deciding which is which (see this related question).
如何确定线程阻塞操作是否会在 STA 上泵送 COM 消息?
How can I determine if a thread-blocking operation will pump COM messages on an STA?
到目前为止的部分列表:
Partial lists so far:
阻止进行抽水的操作*:
Thread.Join
WaitHandle.WaitOne
/WaitAny
/WaitAll
(WaitAll
不能从 STA 线程调用)GC.WaitForPendingFinalizers
Monitor.Enter
(因此lock
)- 在某些情况下ReaderWriterLock
- BlockingCollection
Thread.Join
WaitHandle.WaitOne
/WaitAny
/WaitAll
(WaitAll
cannot be called from an STA thread though)GC.WaitForPendingFinalizers
Monitor.Enter
(and thereforelock
) - under some conditionsReaderWriterLock
- BlockingCollection
阻止不抽水的操作:
Thread.Sleep
Console.ReadKey
(在某处阅读)
Thread.Sleep
Console.ReadKey
(read it somewhere)
*注意 Noseratio 的回答 说即使是抽水操作,也只能针对非常有限的未公开的 COM-具体消息.
*Note Noseratio's answer saying that even operations which do pump, do so for a very limited undisclosed set of COM-specific messages.
推荐答案
BlockingCollection
确实会在阻塞时抽水.我在回答以下问题时了解到这一点,其中有一些关于 STA 泵送的有趣细节:
BlockingCollection
will indeed pump while blocking. I've learnt that while answering the following question, which has some interesting details about STA pumping:
StaTaskScheduler 和 STA 线程消息泵送
但是,它将发送一组非常有限的未公开的 COM 特定消息,与您列出的其他 API 相同.它不会发送通用 Win32 消息(一个特殊情况是 WM_TIMER
,也不会发送).这可能是一些需要全功能消息循环的 STA COM 对象的问题.
However, it will pump a very limited undisclosed set of COM-specific messages, same as the other APIs you listed. It won't pump general purpose Win32 messages (a special case is WM_TIMER
, which won't be dispatched either). This might be a problem for some STA COM objects which expect a full-featured message loop.
如果您想对此进行试验,请创建自己的 SynchronizationContext
版本,覆盖 SynchronizationContext.Wait
,调用SetWaitNotificationRequired
并安装您的自定义STA 线程上的同步上下文对象.然后在 Wait
中设置断点,看看哪些 API 会使其被调用.
If you like to experiment with this, create your own version of SynchronizationContext
, override SynchronizationContext.Wait
, call SetWaitNotificationRequired
and install your custom synchronization context object on an STA thread. Then set a breakpoint inside Wait
and see what APIs will make it get called.
WaitOne
的标准抽水行为实际上在多大程度上受到限制? 下面是一个导致 UI 线程死锁的典型示例.我在这里使用 WinForms,但同样的问题也适用于 WPF:
To what extent the standard pumping behavior of WaitOne
is actually limited? Below is a typical example causing a deadlock on the UI thread. I use WinForms here, but the same concern applies to 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 毫秒的时间流逝,尽管任务只需 2000 毫秒即可完成.
The message box will show the time lapse of ~ 4000 ms, although the task takes only 2000 ms to complete.
发生这种情况是因为 await
延续回调是通过 WindowsFormsSynchronizationContext.Post
安排的,它使用 Control.BeginInvoke
,而 Control.BeginInvoke
又使用 PostMessage
,发布使用 RegisterWindowMessage
注册的常规 Windows 消息.此消息不会被发送并且 handle.WaitOne
超时.
That happens because the await
continuation callback is scheduled via WindowsFormsSynchronizationContext.Post
, which uses Control.BeginInvoke
, which in turn uses PostMessage
, posting a regular Windows message registered with RegisterWindowMessage
. This message doesn't get pumped and handle.WaitOne
times out.
如果我们使用 handle.WaitOne(Timeout.Infinite)
,我们就会遇到典型的死锁.
If we used handle.WaitOne(Timeout.Infinite)
, we'd have a classic deadlock.
现在让我们实现一个带有显式泵送的 WaitOne
版本(并将其称为 WaitOneAndPump
):
Now let's implement a version of WaitOne
with explicit pumping (and call it 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);
}
}
}
}
并像这样更改原始代码:
And change the original code like this:
var startTick = Environment.TickCount;
handle.WaitOneAndPump(4000);
MessageBox.Show("Lapse: " + (Environment.TickCount - startTick));
现在的时间间隔约为 2000 毫秒,因为 await
继续消息由 Application.DoEvents()
泵送,任务完成并发出其句柄信号.
The time lapse now will be ~2000 ms, because the await
continuation message gets pumped by Application.DoEvents()
, the task completes and its handle is signaled.
也就是说,我从不建议将 WaitOneAndPump
之类的东西用于生产代码(除了极少数特定情况).它是 UI 重入等各种问题的根源.这些问题是微软将标准泵送行为仅限于某些特定于 COM 的消息的原因,这对于 COM 编组至关重要.
That said, I'd never recommend using something like WaitOneAndPump
for production code (besides for very few specific cases). It's a source of various problems like UI re-entrancy. Those problems are the reason Microsoft has limited the standard pumping behavior to only certain COM-specific messages, vital for COM marshaling.
这篇关于哪些阻塞操作会导致 STA 线程泵送 COM 消息?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持编程学习网!