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毫秒的时间差,尽管任务只需2000毫秒即可完成。这是因为使用了“WindowsFormsSynchronizationContext.Post”调度“await”继续回调,该回调使用“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)
{
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;
else if (result == WAIT_TIMEOUT)
return false;
else if (result == WAIT_ABANDONED_0)
throw new AbandonedMutexException(-1, handle);
else if (result != WAIT_OBJECT_0 + 1)
throw new InvalidOperationException();
else
{
if (timeout == 0)
return false;
else
{
Application.DoEvents();
Application.RaiseIdle(EventArgs.Empty);
}
}
}
}
将原始代码更改为以下内容:
var startTick = Environment.TickCount;
handle.WaitOneAndPump(4000);
MessageBox.Show("Lapse: " + (Environment.TickCount - startTick));
现在的时间延迟约为2000毫秒,因为通过使用
Application.DoEvents()
来推送
await
继续消息,任务完成并且其句柄被信号标记。
尽管如此,
我不建议在生产代码中使用类似WaitOneAndPump
的东西(除非是极少数特定情况)。它会导致各种问题,比如UI重新进入。这些问题是微软限制标准泵送行为仅适用于某些COM特定消息的原因。
BlockingCollection.Take()
实际上是如何阻塞的。 - bavaza