1

One downside of the async pattern in C# 5 is that Tasks are not covariant, i.e., there isn't any ITask<out TResult>.

I have noticed that my developers often do

return await SomeAsyncMethod();

to come around this.

Exactly what impact performance-wise will this create? There isn't any I/O or thread yield. It will just await and cast it to the correct covariant. What will the async framework do under the hood in this case? Will there be any Thread context switch?

This code won’t compile:

public class ListProductsQueryHandler : IQueryHandler<ListProductsQuery, IEnumerable<Product>>
{
    private readonly IBusinessContext _context;

    public ListProductsQueryHandler(IBusinessContext context)
    {
        _context = context;
    }

    public Task<IEnumerable<Product>> Handle(ListProductsQuery query)
    {
        return _context.DbSet<Product>().ToListAsync();
    }
}

because Task is not covariant, but adding await and it will cast it to the correct IEnumerable<Product> instead of List<Product> that ToListAsync returns.

ConfigureAwait(false) everywhere in the domain code does not feel like a viable solution, but I will certainly use it for my low-level methods like

public async Task<object> Invoke(Query query)
{
    var dtoType = query.GetType();
    var resultType = GetResultType(dtoType.BaseType);
    var handler = _container.GetInstance(typeof(IQueryHandler<,>).MakeGenericType(dtoType, resultType)) as dynamic;
    return await handler.Handle(query as dynamic).ConfigureAwait(false);
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Anders
  • 17,306
  • 10
  • 76
  • 144

4 Answers4

2

The more notable cost of your approach to solving this problem is that if there is a value in SynchronizationContext.Current you need to post a value to it and wait for it to schedule that work. If the context is busy doing other work, you could be waiting for some time when you don't actually need to do anything in that context.

That can be avoided by simply using ConfigureAwait(false), while still keeping the method async.

Once you've removed the possibility of using the sync context, then the state machine generated by the async method shouldn't have an overhead that's notably higher than what you'd need to provide when adding the continuation explicitly.

Servy
  • 202,030
  • 26
  • 332
  • 449
  • Syntax sugar wise is sad,but nice there is a solution (I still think they should have made it Covariant) – Anders Aug 19 '15 at 13:17
  • Btw, this will not affect the DB query? I want thread context switch for the DB call since otherwisemy thread pool can be filled – Anders Aug 19 '15 at 13:18
  • @Anders I agree that I would have preferred them to have had an `ITask` as the center of the TPL rather than a concrete task, although it being covariant is only one of many reasons that would be nicer, but that's the value of hindsight. – Servy Aug 19 '15 at 13:19
  • @Anders You're not doing anything in that method that would require the use of the current context, so having it accomplishes nothing except to waste time getting scheduled to run there. The actual work that you have is IO work, not CPU bound work, and so it won't be consuming significant resources from the thread pool. – Servy Aug 19 '15 at 13:21
  • Yes I know, but ConfigureAwait only affect the outer Task right, not the DB task? How could it btw, its allready executed – Anders Aug 19 '15 at 13:22
  • @Anders Even better, it only affects the continuations made in the same async method - from the outside of that, any marshalling between synchronization contexts will remain exactly the same. The only case where this could be a problem would be where the inner task is on a UI context, then you switch to thread pool, and then back to UI. – Luaan Aug 19 '15 at 13:24
  • @Anders It affects the continuation, of which there is only one in your method, and adding `ConfigureAwait(false)` means that the one continuation that's there won't schedule it in the synchronization context. – Servy Aug 19 '15 at 13:24
  • Hmm, exactly how should I use ConfigureAwait in real life example above? – Anders Aug 19 '15 at 13:26
  • @Anders It's as simple as `return await SomeAsyncMethod().ConfigureAwait(false);`. – Luaan Aug 19 '15 at 13:30
  • I think the syntax impact kills the purpose, but I will certenly use this for my low lever CQS methods, thanks – Anders Aug 19 '15 at 13:46
2

Exactly what impact performance-wise will this create?

Almost none.

Will there be any Thread context switch?

Not any more than normal.

As I describe on my blog, when an await decides to yield, it will first capture the current context (SynchronizationContext.Current, unless it is null, in which case the context is TaskScheduler.Current). Then, when the task completes, the async method resumes executing in that context.

The other important element in this conversation is that task continuations execute synchronously if possible (again, described on my blog). Note that this is not actually documented anywhere; it's an implementation detail.

ConfigureAwait(false) everywhere in domain code does not feel like a viable solution

You can use ConfigureAwait(false) in your domain code (and I actually recommend you do, for semantic reasons), but it probably will not make any difference performance-wise in this scenario. Here's why...

Let's consider how this call works within your application. You've undoubtedly got some entry-level code that depends on the context - say, a button click handler or an ASP.NET MVC action. This calls into the code in question - domain code that performs an "asynchronous cast". This in turn calls into low-level code which is already using ConfigureAwait(false).

If you use ConfigureAwait(false) in your domain code, the completion logic will look like this:

  1. Low-level task completes. Since this is probably I/O-based, the task completion code is running on a thread pool thread.
  2. The low-level-task completion code resumes executing the domain-level code. Since the context was not captured, the domain-level code (the cast) executes on the same thread pool thread, synchronously.
  3. The domain-level code reaches the end of its method, which completes the domain-level task. This task completion code is still running on the same thread pool thread.
  4. The domain-level-task completion code resumes executing the entry-level code. Since the entry level requires the context, the entry-level code is queued to that context. In a UI app, this causes a thread switch to the UI thread.

If you don't use ConfigureAwait(false) in your domain code, the completion logic will look like this:

  1. Low-level task completes. Since this is probably I/O-based, the task completion code is running on a thread pool thread.
  2. The low-level-task completion code resumes executing the domain-level code. Since the context was captured, the domain-level code (the cast) is queued to that context. In a UI app, this causes a thread switch to the UI thread.
  3. The domain-level code reaches the end of its method, which completes the domain-level task. This task completion code is running in the context.
  4. The domain-level-task completion code resumes executing the entry-level code. The entry level requires the context, which is already present.

So, it's just a matter of when that context switch takes place. For UI apps, it is nice to keep small amounts of work on the thread pool for as long as possible, but for most apps it won't hurt performance if you don't. Similarly, for ASP.NET apps, you can get tiny amounts of parallelism for free if you keep as much code as possible out of the request context, but for most apps this won't matter.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Thanks for the input. What I meant with keep it out of the domain is because it should not bother with such matters (How tasks are executed). It should only bother with domain logic. For readability, Separation of concerenrc etc. But in the case of Tasks I guess there is no way around it and I will absolute use it were it makes sense. For example I now use it close to the metal in my CQS API. Btw, this is a WebAPI enabled CQS project running in ASP.NET – Anders Aug 24 '15 at 07:23
  • 2
    @Anders: Yeah, the `ConfigureAwait(false)` actually means "I *don't* care about context". It's unfortunate that you have to explicitly state that, instead of having it as the default. – Stephen Cleary Aug 24 '15 at 13:49
  • I have an additional question to you since I know you are the king of Tasks. Please look at my edit at bottom of question. It shows how my CQS API uses configure await. Lets say the Handle method directly returns the Task from EF ToListAsync(). Will my code not reconfigure it not to care about context? Which is bad since it wont scale. Or will it configure the outer "cast" await? – Anders Aug 25 '15 at 09:17
  • @Anders: It's Configure*Await*, not Configure*Task*. It should only be called at the point of an `await`. – Stephen Cleary Aug 25 '15 at 11:30
  • yes, but its a bit unclear, you chain the ConfigureAwait directly on the result from the async method. So its unclear if its actually the task returned in the method or if its the outer await that is configured. Its a bit unclear and there is a huge difference since the inner Task could be a I/O task but the outer is always a simple cast task – Anders Aug 25 '15 at 12:41
  • @Anders: Yes, but if you try to return the result of `ConfigureAwait(false)` instead of awaiting it, you'll find something interesting: it does not return a task. – Stephen Cleary Aug 25 '15 at 14:45
  • So whats really happening is (await MyMethod()).ConfigureAwait(false) (this code wont compile btw) – Anders Aug 25 '15 at 16:09
  • @Anders: No, it's `var task = MyMethod(); var configuredAwaitable = task.ConfigureAwait(false); await configuredAwaitable;` – Stephen Cleary Aug 25 '15 at 16:52
  • Thats not good?, if the MyMethod task is spawned by I/O it means it wont context switch? I want the outer "cast" await to be configured. Here is another example were I now use ConfigureAwait = false, here its also importat that its the "cast" task that is configured not the "inner" task http://pastebin.com/aWyLyrNW – Anders Aug 26 '15 at 07:34
  • 1
    @Anders: Try (re-)reading my [async best practices](https://msdn.microsoft.com/en-us/magazine/jj991977.aspx) article. Your paste does absolutely nothing, because the caller would still have to use `ConfigureAwait(false)` to avoid *their* context capture. – Stephen Cleary Aug 26 '15 at 12:51
1

There is some overhead associated, though most of the time, there will not be a context switch - most of the cost is in the state machine generated for each async method with awaits. The main problem would be if there's something preventing the continuation from running synchronously. And since we're talking I/O operations, the overhead will tend to be entirely dwarfed by the actual I/O operation - again, the major exception being synchronization contexts like the one Windows Forms uses.

To lessen this overhead, you could make yourself a helper method:

public static Task<TOut> Cast<TIn, TOut>(this Task<TIn> @this, TOut defaultValue)
  where TIn : TOut
{
    return @this.ContinueWith(t => (TOut)t.GetAwaiter().GetResult());
}

(the defaultValue argument is only there for type inference—sadly, you will have to write out the return type explicitly, but at least you don't have to type out the "input" type manually as well)

Sample usage:

public class A
{
    public string Data;
}

public class B : A { }

public async Task<B> GetAsync()
{
    return new B { Data =
     (await new HttpClient().GetAsync("http://www.google.com")).ReasonPhrase };
}

public Task<A> WrapAsync()
{
    return GetAsync().Cast(default(A));
}

There is a bit of tweaking you can try if needed, e.g. using TaskContinuationOptions.ExecuteSynchronously, which should work well for most task schedulers.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Luaan
  • 62,244
  • 7
  • 97
  • 116
0

There will be a state machine generated by the compiler which saves the current SynchornizationContext when calling the method and restores it after the await.

So there could be a context switch, for example when you call this from a UI thread, the code after the await will switch back to run on the UI thread which will result in a context switch.

This article might be useful Asynchronous Programming - Async Performance: Understanding the Costs of Async and Await

In your case it may be handy to stick a ConfigureAwait(false) after the await to avoid the context switch, however the state machine for the async method will be still generated. In the end the cost of the await will be negligible in comparison to your query cost.

NeddySpaghetti
  • 13,187
  • 5
  • 32
  • 61