Search code examples
c#comasync-awaitactivexrcw

What circumstances other than "await" will allow synchronous code to be interrupted


I recently came across a strange bug in my async code. I was calling a blocking method on a COM control, which appeared to allow my async continuations to run while it was blocking.

Consider example code (illustrative purposes only)

public async void Main()
{
    //assume some single threaded dispatcher. eg. WpfDispatcher

    Task doAsyncTask = DoAsync();

    Console.WriteLine("Start synchronous");
    CallSomeCOMControl();
    Console.WriteLine("End synchronous");

    await doAsyncTask;
}

public async Task DoAsync()
{
    Console.WriteLine("Start async");
    await Task.Delay(1);
    Console.WriteLine("End async");
}

In normal circumstances I would expect the following output:

Start async
Start synchronous
End synchronous
End async

What I was effectively seeing was:

Start async
Start synchronous
End async
End synchronous

Now I do not have the source for the COM control, but I do know that is a very old C++ control that has no notion of async/await. It is however an Active-X control.

I do not know the implementation details of the .Net RCW, but I am assuming that some kind of message pumping must be going on to allow the control to work. I am also assuming that this pumping is allowing my continuations to run.

Are my assumptions correct? And are the any other circumstances that I should be aware of where my supposedly synchronous code could get interrupted?


Solution

  • This is pretty normal, COM will pump when the call crosses an apartment boundary. It has to pump, not doing so is very likely to cause deadlock. It is unclear why that boundary has to be crossed from the snippet, it looks like an STA thread when you talk about WPF. Could be an out-of-process server, could be that you created the object on a worker thread.

    It is not fundamentally different from the way the CLR will pump on a WaitOne() or Join() call when you block an STA thread. The re-entrancy headaches that this causes are pretty similar to the kind of misery caused by DoEvents(). Both COM and the CLR pump selectively though, not quite as dangerous. Albeit highly undocumented.

    COM apartments greatly simplify threading, taking care of 98% of the usual problems. The last 2% give you a splitting migraine that can be exceedingly hard to solve, re-entrancy is on the top of that list. Only way to address that is to not leave it up to COM to provide the component with a thread-safe home and take care of it yourself. With code like this.