18

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

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

How can I determine if a thread-blocking operation will pump COM messages on an STA?

Partial lists so far:

Blocking operations which do pump*:

Blocking operations which do not pump:

*Note Noseratio's answer saying that even operations which do pump, do so for a very limited undisclosed set of COM-specific messages.

Community
  • 1
  • 1
bavaza
  • 10,319
  • 10
  • 64
  • 103
  • 1
    The assertion "In general, COM object must be instantiated on STA" is incorrect. There is no "generality" and no "must", as it really depends on the COM object, how it's declared to COM. Indeed, COM does all the work for you to avoid these "must" (sometimes at the cost of unwanted marshaling allright). – Simon Mourier Feb 05 '14 at 09:27
  • Classes that wrap an unseen synchronization object, like BlockingCollection, fall in the WaitHandle.Wait bracket. – Hans Passant Feb 05 '14 at 10:39
  • @HansPassant - how can one tell which type of sych object the wrapper class uses? For example, the docs do not specify how `BlockingCollection.Take()` actually blocks. – bavaza Feb 05 '14 at 11:56
  • You can look at the Reference Source or use a decompiler. It doesn't actually matter, any waits on a sync object end up using the same code inside the CLR. – Hans Passant Feb 05 '14 at 12:15

3 Answers3

6

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.

Community
  • 1
  • 1
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • Can this be tweaked to resolve http://stackoverflow.com/questions/22794774/how-to-handle-this-thread-issue? – LCJ Apr 09 '14 at 04:01
  • @Lijo, it can easily be tweaked to resolve that, but it would be conceptually wrong. You almost never need to use `DoEvents`, check [this](http://blogs.msdn.com/b/jfoscoding/archive/2005/08/06/448560.aspx). Why don't you want to use `async`/`await`, I think we discussed that before? – noseratio Apr 09 '14 at 04:06
  • I tired async/await - but due to my lack of experience in threading area, I ran into n number of problems and dropped that approach after three days work.. Do you have code that can make the following to async/await? http://stackoverflow.com/questions/22794774/how-to-handle-this-thread-issue – LCJ Apr 09 '14 at 04:18
  • When I am using `WaitOneAndPump` in a class it is not compiling. Error - `The name 'MsgWaitForMultipleObjectsEx' does not exist in the current context` - Can you please tell what are the steps to be done to make the WaitOneAndPump compile in a blank class – LCJ Apr 09 '14 at 04:24
  • 2
    @Lijo, I do, I think I gave you the [link](http://stackoverflow.com/a/22262976/1768303) already. But it wouldn't help if you don't have enough skills for async programming (there's no threading involved btw, only asynchrony). I suggest you edit that question and openly say you need to get that project done ASP without `async/await` learning curve. Then me or someone else may be able to provide an alternative solution (e.g mere callbacks). – noseratio Apr 09 '14 at 04:31
  • 1
    @Lijo, you can copy `MsgWaitForMultipleObjectsEx` from [here](http://stackoverflow.com/a/21655620/1768303). But again, you're going the wrong path. – noseratio Apr 09 '14 at 04:34
  • Though it is not the best way, what should be the passed for the function `WaitOneAndPump` based on http://stackoverflow.com/questions/22794774/how-to-handle-this-thread-issue? – LCJ Apr 09 '14 at 04:49
  • 1
    @Lijo, that'd be a `ManualResetEvent` object which you'd signal from the `DocumentCompleted` handler. `WaitOneAndPump` would create a nested message loop with all dangerous consequences. – noseratio Apr 09 '14 at 04:53
  • Thanks. Following seems to be working - `var dummy = new ManualResetEvent(false); WaitExt.WaitOneAndPump(dummy, milliSecondsWait);`. I would like to grant you the bounty on my question - http://stackoverflow.com/questions/22794774/how-to-handle-this-thread-issue. Could you post this approach as an answer? (With it's `pros and cons` ).... If you add async/await also along with that , there is nothing like that. – LCJ Apr 09 '14 at 12:50
  • 1
    @Lijo, as much as I would like to take on your bounty, I couldn't recommend `WaitOneAndPump` as an ultimate solution, that'd be wrong. My approach to that problem is [this](http://stackoverflow.com/a/22262976/1768303), feel free to up-vote it instead. Anyhow, I'm glad if I helped you. Keep that question hanging on, just in case someone posts a solution you'll like more. – noseratio Apr 09 '14 at 13:09
3

How the pumping works is actually disclosed. There are internal calls to the .NET runtime which in turn use CoWaitForMultipleHandles to perform the wait on STA threads. The documentation for that API is pretty lacking, but reading some COM books and the Wine source code could give you some rough ideas.

Internally it calls MsgWaitForMultipleObjectsEx with the QS_SENDMESSAGE | QS_ALLPOSTMESSAGE | QS_PAINT flags. Let's dissect what each one is used for.

QS_PAINT is the most obvious, WM_PAINT messages are processed in the message pump. Thus it is really bad idea to do any locking in paint handlers because it will likely get into re-entrant loop and cause a stack overflow.

QS_SENDMESSAGE is for messages sent from other threads and applications. This is actually one way of how the interprocess communication works. The ugly part is that it is also used for UI messages from Explorer and Task Manager, so it pumps WM_CLOSE message (right-click on a non-responsive application in the taskbar and select Close), tray icon messages and possibly something else (WM_ENDSESSION).

QS_ALLPOSTMESSAGE is for the rest. The messages are actually filtered, so only messages for the hidden apartment window and DDE messages (WM_DDE_FIRST - WM_DDE_LAST) are processed.

Filip Navara
  • 4,818
  • 1
  • 26
  • 37
2

I recently learnt the hard way that Process.Start may pump. I did not wait for the process nor ask its pid, I just wanted it to run alongside.

In the calls stacks (I don't have at hand) I saw it go into ShellInvoke-specific code, so this might only apply to ShellInvoke = true.

While the whole STA pumping is surprising enough, I found this one to be highly surprising, to say the least!

Simon Thum
  • 576
  • 4
  • 15