12

I'm currently writing a lot of async library code, and I'm aware of the practice of adding ConfigureAwait(false) after each async call so as to avoid marshalling the continuation code back to the original (usually UI) thread context. As I don't like the unlabelled Boolean parameter, I tended to write this as ConfigureAwait(continueOnCapturedContext: false) instead.

I added an extension method to make it a little more readable (and reduce the typing somewhat):

public static class TaskExtensions
{
    public static ConfiguredTaskAwaitable<TResult> WithoutCapturingContext<TResult>(this Task<TResult> task)
    {
        return task.ConfigureAwait(continueOnCapturedContext: false);
    }

    public static ConfiguredTaskAwaitable WithoutCapturingContext(this Task task)
    {
        return task.ConfigureAwait(continueOnCapturedContext: false);
    }
}

So now I can have something like await SomethingAsync().WithoutCapturingContext() instead of await SomethingAsync().ConfigureAwait(continueOnCapturedContext: false). I consider it an improvement, however even this starts to grate when I have to call a number of async methods in the same block of code, as I end up with something similar to this:

await FooAsync().WithoutCapturingContext();
var bar = await BarAsync().WithoutCapturingContext();
await MoreFooAsync().WithoutCapturingContext();
var moreBar = await MoreBarAsync().WithoutCapturingContext();
// etc, etc

In my opinion it's starting to make the code a lot less readable.

My question was basically this: is there a way of reducing this down further (other than shortening the name of the extension method)?

Steven Rands
  • 5,160
  • 3
  • 27
  • 56
  • Do you always label all your parameters? – i3arnon Nov 24 '14 at 16:21
  • You could move the `ConfigureAwait()` to a lower level. eg. create a wrapper around `BarAsync()` which is basically `return await BarAsync().WithoutCapturingContext()`. – Tejas Sharma Nov 24 '14 at 16:22
  • @I3arnon It varies, but I label Boolean parameters almost always. Although in this case it's such a well-known pattern that there's a good argument for _not_ labelling the parameter. – Steven Rands Nov 24 '14 at 16:38
  • @TejasSharma I'm not quite sure what you mean. Would you be able to knock up a quick answer with a code example? – Steven Rands Nov 24 '14 at 16:43
  • 1
    Don't you need only the first `.WithoutCapturingContext()` anyway? The first invocation would be sufficient to "get away from" the original context; whether subsequent continuations run on the same non-original context or not shouldn't matter much... right? (I mean, the first invocation will likely callback the remainder of the method on the thread pool, and all further callbacks can run in the threadpool too, so you *could* let the awaiter capture the threadpool context.) – stakx - no longer contributing Nov 24 '14 at 17:17
  • 3
    @stakx - no. There is no guarantees that method will return asynchronously (i.e. `return Task.FromResult(42);` is perfectly valid return for a potentially async method). So you have to specify `ConfigureAwait(false)` on every call if you decided to go that route. – Alexei Levenkov Nov 24 '14 at 23:24
  • @AlexeiLevenkov: Good point. In that case, wouldn't it be simpler to shift all code specifying `.ConfigureAwait(false)` simply into a separate task / async method and `Task.Run(…)` *that*? This wouldn't say, "you may change the context for the remainder of the method"; it would likely change the context right from the start (which may be somewhat overkill, but prevent the `.ConfigureAwait(false)` from becoming necessary). But I may again be wrong on this. – stakx - no longer contributing Nov 25 '14 at 07:01
  • 5
    @stakx then you're adding a noticable amount of work to be done at execution time just so that you have less typing to do as a programmer. It means all of your async methods will need to wait for a thread pool thread to be able to execute the code before you can *start* processing any of the other work, and you're adding an additional continuation at the end as well. When you start doing that for almost all of your asynchronous methods, and from library code intended to be used in a number of different contexts (potentially performance sensitive) that becomes a problem. – Servy Nov 25 '14 at 14:59
  • @Servy, Alexei: OK, I'm convinced. :) Thanks for explaining this to me. +1 to both of you. – stakx - no longer contributing Nov 25 '14 at 15:38

3 Answers3

10

There is no global setting to prevent the tasks within the method from capturing the synchronization context, but what you can do is change the synchronization context for just the scope of that method. In your specific case you could change the context to the default sync context for just the scope of that method.

It's easy enough to write a simple disposable class that changes the sync context and then changes it back when disposed:

public class SyncrhonizationContextChange : IDisposable
{
    private SynchronizationContext previous;
    public SyncrhonizationContextChange(SynchronizationContext newContext = null)
    {
        previous = SynchronizationContext.Current;
        SynchronizationContext.SetSynchronizationContext(newContext);
    }

    public void Dispose()
    {
        SynchronizationContext.SetSynchronizationContext(previous);
    }
}

allowing you to write:

using(var change = new SyncrhonizationContextChange())
{
    await FooAsync();
    var bar = await BarAsync();
    await MoreFooAsync();
    var moreBar = await MoreBarAsync();
}

(Note setting the context to null means it'll use the default context.)

Servy
  • 202,030
  • 26
  • 332
  • 449
  • I do like this but after Googling around a bit it seems rarely used and kind of non-standard. Would there be any side-effects to going down this route? Also, what would the "default context" mean in this case? – Steven Rands Nov 25 '14 at 09:36
  • Also, how would this affect the called async methods? For example, if an async method was called from within `FooAsync`, would it get the "null" `SynchronizationContext`, or would I have to replicate the `SynchronizationContextChange` within `FooAsync` as well? – Steven Rands Nov 25 '14 at 09:52
  • 1
    @StevenRands The default context is exactly the context used when you use `ConfigureAwait(false)`. You're specifically telling it to not "remember" the current context at the time the continuation was scheduled, and to instead use the default context, which will use the thread pool. Rather than having every single async method not use the current context, this is simply setting the current context to the context that you want to use. When setting the current context it will stay that way until it's set to something else, which in this case is when it leaves the `using` block. – Servy Nov 25 '14 at 14:55
  • Would you agree that if we don't need the original context for the entire execution of the method, then restoring it is actually unnecessary? Surely the caller's awaiter already captured it if it needs it in its own continuation; no point in storing and restoring it twice. – tne Dec 19 '14 at 19:10
  • @tne Yes, if you don't need the original context in the scope of that method you don't need to restore it. Note though that restoring the context is literally just setting the value of a static variable. It's literally the simplest possible operation that you could ever possibly have to do, so trying to omit it isn't exactly going to save you a lot. Adding a continuation so that the code will resume executing in a particularly thread is another matter entirely from just setting the context. – Servy Jan 02 '15 at 15:57
  • @Servy Yes, I only mentioned this because you explicitly wrote "scope of that method" in your answer -- in which case you don't have to worry about restoring, don't need the `using`-based construct (which is still very nice for arbitrary scope though of course) and hence can save an object allocation and a call to [`SetSynchronizationContext`](http://referencesource.microsoft.com/#mscorlib/system/threading/synchronizationcontext.cs,8b95d330c4c6af9d) (which might not be entirely trivial by the way, I don't think the call to `GetMutableExecutionContext` can/should be inlined). – tne Jan 04 '15 at 03:24
5

Note that ConfigureAwait(false) doesn't mean ignore the synchronization context. Sometimes, it can push the await continuation to a pool thread, despite the actual continuation has been triggered on a non-pool thread with non-null synchronization context. IMO, this behavior of ConfigureAwait(false) can be surprising and non-intuitive. At the very least, its side effect would be a redundant thread switch.

If you really want to ignore the continuation thread's synchronization context after await and just resume the execution synchronously (TaskContinuationOptions.ExecuteSynchronously) on whatever that thread/context happened to be, you could use a custom awaiter:

await MoreFooAsync().IgnoreContext();

Below is a possible implementation of IgnoreContext (only very slightly tested):

public static class TaskExt
{
    // Generic Task<TResult>

    public static IgnoreContextAwaiter<TResult> IgnoreContext<TResult>(this Task<TResult> @task)
    {
        return new IgnoreContextAwaiter<TResult>(@task);
    }

    public struct IgnoreContextAwaiter<TResult> :
        System.Runtime.CompilerServices.ICriticalNotifyCompletion
    {
        Task<TResult> _task;

        public IgnoreContextAwaiter(Task<TResult> task)
        {
            _task = task;
        }

        // custom Awaiter methods
        public IgnoreContextAwaiter<TResult> GetAwaiter()
        {
            return this;
        }

        public bool IsCompleted
        {
            get { return _task.IsCompleted; }
        }

        public TResult GetResult()
        {
            // result and exceptions
            return _task.GetAwaiter().GetResult();
        }

        // INotifyCompletion
        public void OnCompleted(Action continuation)
        {
            // not always synchronous, http://blogs.msdn.com/b/pfxteam/archive/2012/02/07/10265067.aspx
            _task.ContinueWith(_ => continuation(), TaskContinuationOptions.ExecuteSynchronously);
        }

        // ICriticalNotifyCompletion
        public void UnsafeOnCompleted(Action continuation)
        {
            // why SuppressFlow? http://blogs.msdn.com/b/pfxteam/archive/2012/02/29/10274035.aspx
            using (ExecutionContext.SuppressFlow())
            {
                OnCompleted(continuation);
            }
        }
    }

    // Non-generic Task

    public static IgnoreContextAwaiter IgnoreContext(this Task @task)
    {
        return new IgnoreContextAwaiter(@task);
    }

    public struct IgnoreContextAwaiter :
        System.Runtime.CompilerServices.ICriticalNotifyCompletion
    {
        Task _task;

        public IgnoreContextAwaiter(Task task)
        {
            _task = task;
        }

        // custom Awaiter methods
        public IgnoreContextAwaiter GetAwaiter()
        {
            return this;
        }

        public bool IsCompleted
        {
            get { return _task.IsCompleted; }
        }

        public void GetResult()
        {
            // result and exceptions
            _task.GetAwaiter().GetResult();
        }

        // INotifyCompletion
        public void OnCompleted(Action continuation)
        {
            // not always synchronous, http://blogs.msdn.com/b/pfxteam/archive/2012/02/07/10265067.aspx
            _task.ContinueWith(_ => continuation(), TaskContinuationOptions.ExecuteSynchronously);
        }

        // ICriticalNotifyCompletion
        public void UnsafeOnCompleted(Action continuation)
        {
            // why SuppressFlow? http://blogs.msdn.com/b/pfxteam/archive/2012/02/29/10274035.aspx
            using (ExecutionContext.SuppressFlow())
            {
                OnCompleted(continuation);
            }
        }
    }
}
Community
  • 1
  • 1
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • If the asynchronous method completes in the UI thread, and it's important for the continuation to *not* run in the UI thread, then it's actively important to *not* just execute the continuation synchronously. – Servy Nov 26 '14 at 15:18
  • @Servy, if the requirements are such, then it'd be quite easy to tweak this code to account for that inside the `ContinueWith` lambda. However, I'm trying to come up with a real-life scenario for these requirements and so far I'm unable to. E.g., a WinForm's timer or UI element event, wrapped with TCS? I'd still want to handle it synchronously on the UI thread, rather push the continuation to a non-UI thread. – noseratio Nov 26 '14 at 23:02
4

The short answer is no.

You must use ConfigureAwait individually each time, there's no global configuration or something similar. You can, as you said, use an extension method with a shorter name but that doesn't change much. You can probably implement some kind of transformation to your code (maybe using Roslyn) that sticks ConfigureAwait(false) everywhere but it's unreliable in my opinion. In the end it's just something you must write when you need it, like await or ;.

i3arnon
  • 113,022
  • 33
  • 324
  • 344