7

When using await, by default the SynchronizationContext (if one exists) is captured and the codeblocks after the await (continuation blocks) is executed using that context (which results in thread context switches).

public async Task DoSomethingAsync()
{
     // We are on a thread that has a SynchronizationContext here.

     await DoSomethingElseAsync();

     // We are back on the same thread as before here 
     //(well sometimes, depending on how the captured SynchronizationContext is implemented)
}

While this default might make sense in the context of the UI where you want to be back on the UI-thread after an asynchronous operation completes, it doesn't seem make sense as the default for most other scenarios. It certainly doesn't make sense for internal library code, because

  1. It comes with the overhead of unnecessary thread context switches.
  2. It is very easy to accidentaly produce deadlocks (as documented here or here).

It seems to me that Microsoft have decided for the wrong default.

Now my question:

Is there any other (preferably better) way to solve this than by cluttering all await calls in my code with .ConfigureAwait(false)? This is just so easy to forget and makes the code less readable.

Update: Would it maybe suffice to call await Task.Yield().ConfigureAwait(false); at the beginning of each method? If this would guarantee me that I will be on a thread without a SynchronizationContext aferwards, all subsequent await calls would not capture any context.

Wesley Lomax
  • 2,067
  • 2
  • 20
  • 34
bitbonk
  • 48,890
  • 37
  • 186
  • 278
  • 2
    It is an excessively ugly global variable of course. Reveals the true reason async/await was added, it gave programmers a fighting chance to write WinRT code and not die trying. Not the wrong default. As a *general* mechanism to deal with asynchronicity, meh, not really. Any abstraction for threading adds five new hard-to-debug problems. Making it look too easy makes it dangerous. – Hans Passant Aug 28 '15 at 12:30
  • Would you suggest it might be better to avoid async/await in library code alltogehter? – bitbonk Aug 28 '15 at 12:33
  • 1
    Regarding this: `// We are back on the same thread as before here.` - not necessarily on the same thread. It might not be the same thread in ASP.NET, but still the same synchronization context. – noseratio Aug 28 '15 at 12:41
  • @Noseratio You are absolutely right. – bitbonk Aug 28 '15 at 12:42
  • 1
    Task.Yield().ConfigureAwait(false) does not compile. Yield does not return a task, it returns a specially crafted awaitable. And it does not put you off of the sync context. – usr Aug 30 '15 at 21:28

2 Answers2

1

Is there a better way to solve this than by cluttering all await calls in my code with .ConfigureAwait(false)? This is just so easy to forget and makes the code less readable.

Not really. There isn't any "out of the box" switch that you can turn on to change that behavior. There is the ConfigureAwaiter Checker ReSharper extension which can help. The other alternative would be to roll you own custom extension method or SynchronizationContext wrapper which switches the context, or alternatively, even a custom awaiter.

Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
1

To begin with, await Task.Yield().ConfigureAwait(false) won't work because Yield doesn't return a Task. There are other ways of hopping over to a pool thread, but their use isn't recommended either, check "Why was “SwitchTo” removed from Async CTP / Release?"

If you still want to do it, here's a nice trick exploiting the fact that ConfigureAwait(false) pushes the continuation to a pool thread if there is a synchronization context on the original thread, even though there is no asynchrony here:

static Task SwitchAsync()
{
    if (SynchronizationContext.Current == null)
        return Task.FromResult(false); // optimize

    var tcs = new TaskCompletionSource<bool>();
    Func<Task> yield = async () =>
        await tcs.Task.ConfigureAwait(false);
    var task = yield();
    tcs.SetResult(false);
    return task;
}

// ...

public async Task DoSomethingAsync()
{
    // We are on a thread that has a SynchronizationContext here.
    await SwitchAsync().ConfigureAwait(false); 

    // We're on a thread pool thread without a SynchronizationContext 
    await DoSomethingElseAsync(); // no need for ConfigureAwait(false) here
    // ...       
}

Again, this is not something I'd extensively use myself. I've had similar concerns about using ConfigureAwait(false). One outcome was, while ConfigureAwait(false) might not be universally perfect, using it with await as soon as you don't care about synchronization context is the way to go. This is the guideline the .NET source code itself follows closely.

Another outcome was, if you're concerned about a 3rd party code inside DoSomethingElseAsync which may not be using ConfigureAwait(false) correctly, just do the following:

public async Task DoSomethingAsync()
{
    // We are on a thread that has a SynchronizationContext here.
    await Task.Run(() => DoSomethingElseAsync()).ConfigureAwait(false);

    // We're on a thread pool thread without a SynchronizationContext 
    await DoYetSomethingElseAsync(); // no need for ConfigureAwait(false) here
    // ...       
}

This will use the Task.Run override which accepts a Func<Task> lambda, run it on a pool thread and return an unwrapped task. You'd do it only for the first await inside DoSomethingAsync. The potential cost is the same as for SwitchAsync: one extra thread switch, but the code is more readable and better structured. This is the approach I use in my work.

Community
  • 1
  • 1
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • 2
    I believe your `SwitchAsync()` has a race condition: the returned `Task` can complete before it's `await`ed and in that case, the switch doesn't happen. You can observe this by adding `Thread.Sleep(20);` (simulating a long context switch) after `SetResult()`. – svick Aug 29 '15 at 09:56
  • Interestingly, [`AwaitExtensions.SwitchTo`](https://msdn.microsoft.com/en-us/library/microsoft.visualstudio.threading.awaitextensions.switchto.aspx) still exists in Visual Studio SDK. – noseratio Aug 31 '15 at 01:16
  • About _no need for ConfigureAwait(false) here_: https://msdn.microsoft.com/en-us/magazine/jj991977.aspx _if you can use ConfigureAwait at some point within a method, then I recommend you use it for every await in that method after that point. Recall that the context is captured only if an incomplete Task is awaited; if the Task is already complete, then the context isn’t captured. Some tasks might complete faster than expected in different hardware and network situations, and you need to graciously handle a returned task that completes before it’s awaited_ – XanderMK Dec 08 '15 at 20:29