Ответ 1
BlockingCollection
действительно будет накачиваться при блокировке. Я узнал, что, отвечая на следующий вопрос, в котором есть интересные сведения о перекачке STA:
Перекачка сообщений StaTaskScheduler и STA
Однако он будет передавать очень ограниченный нераскрытый набор сообщений, специфичных для COM, как и другие перечисленные вами API. Он не будет передавать общие сообщения Win32 общего назначения (особый случай WM_TIMER
, который также не будет отправлен). Это может быть проблемой для некоторых объектов STA COM, которые ожидают полнофункциональный цикл сообщений.
Если вам нравится экспериментировать с этим, создайте свою собственную версию SynchronizationContext
, переопределите SynchronizationContext.Wait
, вызовите SetWaitNotificationRequired
и установите свой настраиваемый объект контекста синхронизации в потоке STA. Затем установите точку останова внутри Wait
и посмотрите, какие API-интерфейсы заставят ее вызвать.
В какой степени стандартное поведение откачки WaitOne
фактически ограничено? Ниже приведен типичный пример, вызывающий тупик в потоке пользовательского интерфейса. Я использую 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
для производственного кода (к тому же для очень немногих конкретных случаев). Это источник различных проблем, таких как перезапуск пользовательского интерфейса. Эти проблемы являются причиной того, что Microsoft ограничила стандартное поведение откачки только определенными сообщениями, специфичными для COM, жизненно важными для маршалинга COM.