BlockingCollection
确实会在阻塞时抽水。我在回答以下问题时了解到,该问题有一些关于 STA 泵送的有趣细节:
StaTaskScheduler 和 STA 线程消息泵送
但是,它将抽取一组非常有限的未公开的 COM 特定消息,与您列出的其他 API 相同。它不会发送通用 Win32 消息(一个特殊情况是WM_TIMER
,也不会发送)。这对于一些期望全功能消息循环的 STA COM 对象可能是个问题。
如果您想对此进行试验,请在 STA 线程上创建您自己的版本SynchronizationContext
、覆盖SynchronizationContext.Wait
、调用SetWaitNotificationRequired
和安装您的自定义同步上下文对象。然后在里面设置一个断点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 毫秒的时间流逝,尽管该任务只需 2000 毫秒即可完成。
发生这种情况是因为await
继续回调是通过 安排的WindowsFormsSynchronizationContext.Post
,它使用Control.BeginInvoke
,而后者又使用PostMessage
,发布一个注册的常规 Windows 消息RegisterWindowMessage
。此消息不会被抽出并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 编组至关重要。