3

I'm wondering if I use async and await to excessively in my code and if there are (performance) penalties in doing so?

What I often do:

static void Main()
{
    var result = Task<int>.Run (()=> S1Async(1)).Result;
    Console.WriteLine(result);
}

static async Task<int> WrapperAsync(Func<int, Task<int>> func) => await func(2);
static async Task<int> S1Async(int x) => await WrapperAsync(async t=> await S2Async(x * t));
static async Task<int> S2Async(int x) => await WrapperAsync(async t=> await S3Async(x * t));
static async Task<int> S3Async(int x) => await WrapperAsync(async t=> await S4Async(x * t));
static async Task<int> S4Async(int x) => await Task.FromResult(x * 10);

I think the async-awaits can be skipped and that this code is similar:

static void Main()
{
    var result = Task<int>.Run(() => S1Async(1)).Result;
    Console.WriteLine(result);
}

static Task<int> WrapperAsync(Func<int, Task<int>> func) => func(2);
static Task<int> S1Async(int x) => WrapperAsync(t => S2Async(x * t));
static Task<int> S2Async(int x) => WrapperAsync(t => S3Async(x * t));
static Task<int> S3Async(int x) => WrapperAsync(t => S4Async(x * t));
static Task<int> S4Async(int x) => Task.FromResult(x * 10);

When tasks are nested with just one task per level, is it safe to skip async/await?

Both code-samples gives the same result in LinqPad, so I assume they are similiar, but maybe there are side-effects I should be aware of?

Frode
  • 3,325
  • 1
  • 22
  • 32
  • I'd say the biggest issue with this code is mixing async-await with blocking (using `Result` or `Wait`) That practice can cause deadlocks if used incorrectly. Not using async-await and instead returning a Task is fine as long as you don't do that in a try block because if you don't `await` then the exception can be thrown after you leave the try block. – juharr Dec 01 '19 at 16:38
  • What is a penalty ? Is your question, if this is ever becoming slower then to execute everything synchronous ? 5 threads waiting for each other, and you know what you are doing ...it's nothing wrong with that. The threadpool is taking care of, you start not too much threads at a time (But the threshold is at over 100). You can bring yourself in a deadlock with 2 threads, and maybe the chance to do so with uncareful programming may rise on 200 threads. On a single core-CPU async always has a very little penalty against synch execution. But single-core-CPUs only exist on virtual machines today. – Holger Dec 01 '19 at 16:50
  • Play around here: https://sharplab.io/ you will see the difference between the two. The actual cost of the difference depends on the calculation you are doing, but can be quite big if the relation between the 'useful' code and the 'overhead' is bad, – ZorgoZ Dec 01 '19 at 17:26

1 Answers1

1

Short answer is: yes, you can skip them in this case and yes, there's a performance penalty.

When all your method does is await once and immediately return, like this:

async Task FooAsync()
{
    /* ... */

    await BarAsync();
}

then it would be pretty much equivalent to write:

Task FooAsync()
{
    /* ... */

    return BarAsync();
}

The meaning of async is (from the perspective of FooAsync): okay, the operation BarAsync might take a while to complete, so if we get an uncompleted Task from it let me save my current state and return the control flow up to my caller. Once the Task gets completed, I want to pick up the state and continue what I've been doing.

In this case it's clear that there is no additional work that would be performed after the BarAsync Task completes, so you can just return that Task to the caller, since effectively FooAsync completes the exact same moment that Task would. There's no need to save any state or to schedule continuations.

The overhead for this is not huge when we're talking about a single call, but if you call this method many times you might feel an impact. The compiler has to set up the entire async infrastructure the moment you declare your method to be async - the state machine, continuation scheduling, the whole package. So as a general rule: if you can simplify your method to not be async but simply return another Task, it's worth to do so.

As a side note: you certainly don't need to wrap your S1Async call into a Task.Run and you don't have to synchronously block in your Main since C# 7.1. You can just write:

static async Task Main()
{
    var result = await S1Async(1);
    Console.WriteLine(result);
}
V0ldek
  • 9,623
  • 1
  • 26
  • 57