1

I have an async function that is being awaited from the UI thread. I am certain ConfigureAwait(false) is not being called. In fact I've tried explicitly calling ConfigureAwait(true) just to be sure. However, when the awaited Task finishes it is continuing on a worker thread. After some digging I stumbled upon this question, which at least pointed me toward a possible cause. I added

var context = SynchronizationContext.Current;
var scheduler = TaskScheduler.Current;

just before the awaited function so I could see if the TaskAwaiter was able to capture the context. It seems the context is null but the scheduler is assigned a valid reference. However, when the awaited task completed it was still continuing on a worker thread. So I dug a little more.

Using Resharper I found this little gem in Task.SetContinuationForAwait:

// If the user wants the continuation to run on the current "context" if there is one...
if (continueOnCapturedContext)
{
    // First try getting the current synchronization context.
    // If the current context is really just the base SynchronizationContext type, 
    // which is intended to be equivalent to not having a current SynchronizationContext at all, 
    // then ignore it.  This helps with performance by avoiding unnecessary posts and queueing
    // of work items, but more so it ensures that if code happens to publish the default context 
    // as current, it won't prevent usage of a current task scheduler if there is one.
    var syncCtx = SynchronizationContext.CurrentNoFlow;
    if (syncCtx != null && syncCtx.GetType() != typeof(SynchronizationContext))
    {
        tc = new SynchronizationContextAwaitTaskContinuation(syncCtx, continuationAction, flowExecutionContext, ref stackMark);
    }
    else
    {
        // If there was no SynchronizationContext, then try for the current scheduler.
        // We only care about it if it's not the default.
        var scheduler = TaskScheduler.InternalCurrent;
        if (scheduler != null && scheduler != TaskScheduler.Default)
        {
            tc = new TaskSchedulerAwaitTaskContinuation(scheduler, continuationAction, flowExecutionContext, ref stackMark);
        }
    }
}

I set break points on the two if statements and found that both syncCtx and scheduler were null so this explains why the awaited task was continuing on a worker thread but it doesn't explain why SynchronizationContext.Current is null on the UI thread or why TaskScheduler.Current is null at the point the continuation is about to be invoked.

Kenneth Cochran
  • 11,954
  • 3
  • 52
  • 117

1 Answers1

1

It turns out the problem was a result of attempting to use a class library containing WPF forms from a native Win32 application. An ordinary WinForms or WPF application would initialize the SynchronizationContext in the constructor of Control. This would normally occur during the creating of the "parking" form.

For reasons I can't yet explain, no matter how many controls are created, the SynchronizationContext is never initialized. Manually creating it some time before any of the forms are created allows awaited tasks to continue on the correct thread.

if (SynchronizationContext.Current == null)
{
    AsyncOperationManager.SynchronizationContext = new DispatcherSynchronizationContext(Dispatcher.CurrentDispatcher);
}
Kenneth Cochran
  • 11,954
  • 3
  • 52
  • 117
  • I believe WPF only installs its `SynchronizationContext` in its main loop (dispatcher's pushframe or whatever). This makes a lot more sense to me than WinForm's habit of installing its `SyncCtx` on any thread that happens to construct a UI handle. – Stephen Cleary Jan 23 '16 at 05:16