59

If you are using async/await at a lower level in your architecture, is it necessary to "bubble up" the async/await calls all the way up, is it inefficient since you are basically creating a new thread for each layer (asynchronously calling an asynchronous function for each layer, or does it not really matter and is just dependent on your preference?

I'm using EF 6.0-alpha3 so that I can have async methods in EF.

My repository is such:

public class EntityRepository<E> : IRepository<E> where E : class
{
    public async virtual Task Save()
    {
        await context.SaveChangesAsync();
    }
}

Now my business layer is as such:

public abstract class ApplicationBCBase<E> : IEntityBC<E>
{
    public async virtual Task Save()
    {
        await repository.Save();
    }
}

And then of course my method in my UI would have the follow the same pattern when calling.

Is this:

  1. necessary
  2. negative on performance
  3. just a matter of preference

Even if this isn't used in separate layers/projects the same questions applies to if I am calling nested methods in the same class:

    private async Task<string> Dosomething1()
    {
        //other stuff 
        ...
        return await Dosomething2();
    }
    private async Task<string> Dosomething2()
    {
        //other stuff 
        ...
        return await Dosomething3();
    }
    private async Task<string> Dosomething3()
    {
        //other stuff 
        ...
        return await Task.Run(() => "");
    }
valdetero
  • 4,624
  • 1
  • 31
  • 46

2 Answers2

59

If you are using async/await at a lower level in your architecture, is it necessary to "bubble up" the async/await calls all the way up, is it inefficient since you are basically creating a new thread for each layer (asynchronously calling an asynchronous function for each layer, or does it not really matter and is just dependent on your preference?

This question suggests a couple of areas of misunderstanding.

Firstly, you don't create a new thread each time you call an asynchronous function.

Secondly, you don't need to declare an async method, just because you're calling an asynchronous function. If you're happy with the task that's already being returned, just return that from a method which doesn't have the async modifier:

public class EntityRepository<E> : IRepository<E> where E : class
{
    public virtual Task Save()
    {
        return context.SaveChangesAsync();
    }
}

public abstract class ApplicationBCBase<E> : IEntityBC<E>
{
    public virtual Task Save()
    {
        return repository.Save();
    }
}

This will be slightly more efficient, as it doesn't involve a state machine being created for very little reason - but more importantly, it's simpler.

Any async method where you have a single await expression awaiting a Task or Task<T>, right at the end of the method with no further processing, would be better off being written without using async/await. So this:

public async Task<string> Foo()
{
    var bar = new Bar();
    bar.Baz();
    return await bar.Quux();
}

is better written as:

public Task<string> Foo()
{
    var bar = new Bar();
    bar.Baz();
    return bar.Quux();
}

(In theory there's a very slight difference in the tasks being created and therefore what callers could add continuations to, but in the vast majority of cases, you won't notice any difference.)

Daniel A.A. Pelsmaeker
  • 47,471
  • 20
  • 111
  • 157
Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • Note that you may also mark a message such as one you described as `async` to add error handling. If you await the task you can just wrap the call in a `try/catch` block, rather than adding continuations to the provided task. – Servy Mar 19 '13 at 15:47
  • @Servy: Yes, but in that case it wouldn't meet the description of providing no extra processing. It's handy that one *can* do that without changing the API, mind you... – Jon Skeet Mar 19 '13 at 15:53
  • So, trying to understand it, I really only need to use async/await if something that follows in the same function requires the return of the async method? If it is fire and forget or the last statement then there is no need for the await? – valdetero Mar 19 '13 at 16:43
  • 2
    @valdetero: Well in this case it's not really "fire and forget" - you're still returning a `Task` which can be used to see when the operation has completed, failed etc. It's tricky to accurately sum things up in a single sentence - it's best to dive deeper into what await actually does for you. – Jon Skeet Mar 19 '13 at 16:55
  • Jon - is the non-async/non-await version different for what exceptions would result? I usually think of await as unwrapping the AggregateException and throwing the first, but haven't checked whether that's really true for a 'single await at the end' async method or not. – James Manning Mar 20 '13 at 01:39
  • Oh, and since there's no further processing, are they equivalent WRT the continuation scheduling? I think of no-await as being equivalent to await with ConfigureAwait(false), but not sure if that really affects the 'tail async call' situation. Thanks! – James Manning Mar 20 '13 at 01:46
  • @JamesManning: Yes, removing async/await will enable multiple exceptions to be aggregated. In terms of `ConfigureAwait` - well, it may affect which thread the returned task completes on, but if you're awaiting that task anyway, it's unlikely to make a difference. (If it does, your code is too brittle :) – Jon Skeet Mar 20 '13 at 06:48
  • @Jon - thanks! WRT the await, I meant more that 1) if caller has thread affinity (WinForms, Silverlight, etc) then they could textBox.Text = Method().Results and it would work for the async/await version (since no ConfigureAwait(false) but fail with a cross-thread exception with the 'just return the Task that EFv6 gave you' AFAICT. :) – James Manning Mar 20 '13 at 11:19
  • 1
    @JamesManning: No, it would work for the non-async/await version too, because the *calling* code would still be awaiting the task first, and it's *their* await which would bring them back to the right context. They'd still want `textBox.Text = await Method()` - but it doesn't matter whether `Method` is async or just returning the task from something else. – Jon Skeet Mar 20 '13 at 12:04
  • @Jon, is your last code snippet not conflicting with: http://stackoverflow.com/a/10247905/44991 ? – Frans Bouma May 20 '13 at 15:25
  • @FransBouma: I don't see why. That question is talking about a scenario with two awaits, whereas this is only a single await, unless I'm missing something due to jetlag, which is entirely plausible. – Jon Skeet May 20 '13 at 22:43
28

is it inefficient since you are basically creating a new thread for each layer (asynchronously calling an asynchronous function for each layer, or does it not really matter and is just dependent on your preference?

No. Asynchronous methods do not necessarily use new threads. In this case, since the underlying asynchronous method call is an IO bound method, there really should be no new threads created.

Is this:

1. necessary

It is necessary to "bubble" up the async calls if you want to keep the operation asynchronous. This really is preferred, however, as it allows you to fully take advantage of the asynchronous methods, including composing them together across the entire stack.

2. negative on performance

No. As I mentioned, this does not create new threads. There is some overhead, but much of this can be minimized (see below).

3. just a matter of preference

Not if you want to keep this asynchronous. You need to do this to keep things asynchronous across the stack.

Now, there are some things you can do to improve perf. here. If you're just wrapping an asynchronous method, you don't need to use the language features - just return the Task:

public virtual Task Save()
{
    return repository.Save();
}

The repository.Save() method already returns a Task - you don't need to await it just to wrap it back in a Task. This will keep the method somewhat more efficient.

You can also have your "low level" asynchronous methods use ConfigureAwait to prevent them from needing the calling synchronization context:

private async Task<string> Dosomething2()
{
    //other stuff 
    ...
    return await Dosomething3().ConfigureAwait(false);
}

This dramatically reduces the overhead involved in each await if you don't need to worry about the calling context. This is typically the best option when working on "library" code, since the "outer" await will capture the UI's context. The "inner" workings of the library don't typically care about synchronization context, so it's best to not capture that.

Finally, I'd caution against one of your examples:

private async Task<string> Dosomething3()
{
    //other stuff 
    ...
    // Potentially a bad idea!
    return await Task.Run(() => "");
}

If you're making an async method which, internally, is using Task.Run to "create asynchrony" around something that's not itself asynchronous, you're effectively wrapping up synchronous code into an async method. This will use a ThreadPool thread, but can "hide" the fact that it's doing so, effectively making the API misleading. It's often better to leave the call to Task.Run for your highest level calls, and let the underlying methods stay synchronous unless they are truly able to take advantage of asynchronous IO or some means of unloading other than Task.Run. (This isn't always true, but "async" code wrapped over synchronous code via Task.Run, then returned via async/await is often a sign of a flawed design.)

Reed Copsey
  • 554,122
  • 78
  • 1,158
  • 1,373
  • The `Task.Run()` in the last method was just a contrived example for sample purposes. It would/could be any method that return a `Task` or `Task` in this case. I just wanted to show the nesting of methods. – valdetero Mar 19 '13 at 15:49
  • 2
    @valdetero Yes, but the specific example is effectively using an anti-pattern with async/await that people aren't aware of. I thought I'd point that out, though I also mentioned that it's not necessarily wrong, just something to use cautiously. – Reed Copsey Mar 19 '13 at 15:50
  • return Task.CompletedTask; – Rakka Rage Mar 11 '17 at 17:28