[EDITED] This appears to be a bug in the Framework's implementation of Application.DoEvents, which I've reported here. Restoring a wrong synchronization context on a UI thread may seriously affect component developers like me. The goal of the bounty is to draw more attention to this problem and to reward @MattSmith whose answer helped tracking it down.
I'm responsible for a .NET WinForms UserControl
-based component exposed as ActiveX to a legacy unmanaged app, via COM interop. The runtime requirement is .NET 4.0 + Microsoft.Bcl.Async.
The component gets instantiated and used on the app's main STA UI thread. Its implementation utilizes async/await
, so it expects that an instance of a serializing synchronization context has been installed on the current thread (i. e.,WindowsFormsSynchronizationContext
).
Usually, WindowsFormsSynchronizationContext
gets set up by Application.Run
, which is where the message loop of a managed app runs. Naturally, this is not the case for the unmanaged host app, and I have no control over this. Of course, the host app still has its own classic Windows message loop, so it should not be a problem to serialize await
continuation callbacks.
However, none of the solutions I've come up with so far is perfect, or even works properly. Here's an artificial example, where Test
method is invoked by the host app:
Task testTask;
public void Test()
{
this.testTask = TestAsync();
}
async Task TestAsync()
{
Debug.Print("thread before await: {0}", Thread.CurrentThread.ManagedThreadId);
var ctx1 = SynchronizationContext.Current;
Debug.Print("ctx1: {0}", ctx1 != null? ctx1.GetType().Name: null);
if (!(ctx1 is WindowsFormsSynchronizationContext))
SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
var ctx2 = SynchronizationContext.Current;
Debug.Print("ctx2: {0}", ctx2.GetType().Name);
await TaskEx.Delay(1000);
Debug.WriteLine("thread after await: {0}", Thread.CurrentThread.ManagedThreadId);
var ctx3 = SynchronizationContext.Current;
Debug.Print("ctx3: {0}", ctx3 != null? ctx3.GetType().Name: null);
Debug.Print("ctx3 == ctx1: {0}, ctx3 == ctx2: {1}", ctx3 == ctx1, ctx3 == ctx2);
}
Debug output:
thread before await: 1 ctx1: SynchronizationContext ctx2: WindowsFormsSynchronizationContext thread after await: 1 ctx3: SynchronizationContext ctx3 == ctx1: True, ctx3 == ctx2: False
Although it continues on the same thread, the WindowsFormsSynchronizationContext
context I'm installing on the current thread before await
gets reset to the default SynchronizationContext
after it, for some reason.
Why does it get reset? I've verified my component is the only .NET component being used by that app. The app itself does call CoInitialize/OleInitialize
properly.
I've also tried setting up WindowsFormsSynchronizationContext
in the constructor of a static singleton object, so it gets installed on the thread when my managed assembly gets loaded. That didn't help: when Test
is later invoked on the same thread, the context has been already reset to the default one.
I'm considering using a custom awaiter to schedule await
callbacks via control.BeginInvoke
of my control, so the above would look like await TaskEx.Delay().WithContext(control)
. That should work for my own awaits
, as long as the host app keeps pumping messages, but not for awaits
inside any of the 3rd party assemblies my assembly may be referencing.
I'm still researching this. Any ideas on how to keep the correct thread affinity for await
in this scenario would be appreciated.