Which blocking operations cause an STA thread to pump COM messages?

BlockingCollection will indeed pump while blocking. I’ve learnt that while answering the following question, which has some interesting details about STA pumping:

StaTaskScheduler and STA thread message pumping

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.

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.

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));
        };
    }
}

The message box will show the time lapse of ~ 4000 ms, although the task takes only 2000 ms to complete.

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.

If we used handle.WaitOne(Timeout.Infinite), we’d have a classic deadlock.

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));

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.

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.

Leave a Comment