1

I am trying to follow the guidance in this blog about not using await when not needed. And first off, if using { ... } is involved, then yes - use await.

Ok, so for the following code, why does DoItTask() not work? Is returning a Task and not using await only work if there are no uses of await in the method?

    private static async Task<string> ReadOne()
    {
        return await readerOne.ReadAsync();
    }

    private static async Task<string> ReadTwo()
    {
        return await readerTwo.ReadAsync();
    }

    private static async Task<string> ReadThree()
    {
        return await readerOne.ReadAsync();
    }

    private static async Task<string> ReadCombinedAsync(string one, string two, string three)
    {
        return await reader.CombineAsync(one, two, three);
    }

    private static Task<string> ReadCombinedTask(string one, string two, string three)
    {
        return reader.CombineAsync(one, two, three);
    }

    private static async Task<string> DoItAwait()
    {
        string one = await ReadOne();
        string two = await ReadOne();
        string three = await ReadOne();

        return await ReadCombinedAsync(one, two, three);
    }

    private static Task<string> DoItTask()
    {
        string one = await ReadOne();
        string two = await ReadOne();
        string three = await ReadOne();

        return ReadCombinedAsync(one, two, three);
    }

ps - I initially asked this question with a very sloppy example. Apologies for that.

David Thielen
  • 28,723
  • 34
  • 119
  • 193

1 Answers1

2

The following doesn't work, because you cannot use await without async. These keywords always go together when using the Task Parallel Library (TPL):

private static Task<string> DoItTask()
{
    //problem: method is not async - compiler error
    string one = await ReadOne(); 
    string two = await ReadOne();
    string three = await ReadOne();

    // OK, because method is not async
    return ReadCombinedAsync(one, two, three);
}

Asynchronous methods with the async Task signature create a state machine under the hood, which then executes the asynchronous operation without blocking the calling thread. The calling method gets suspended while any Task that is being executed hasn't finished yet. Inside of an asynchronous method, you can only await other asynchronous Tasks or start Tasks without returning them.

This is different when you have a synchronous method. There, the Task can actually be returned directly:

public Task GetSomeTaskAsync()
{
    return SomethingElseAsync();
}

public async Task AwaitTaskAsync()
{
    await GetSomeTaskAsync();
}

This only is useful when you really can just pass along a Task without doing anything else related to that operation, so that you can save some resources, because the state machines of async Task create some overhead. The downside is that you'll lose some stack trace information when exceptions occur.

You can find more useful information here: Why use async and return await, when you can return Task directly?

Coming back to your scenario, your only two options here are:

private static async Task<string> DoItTask()
{
    string one = await ReadOne();
    string two = await ReadOne();
    string three = await ReadOne();

    return await ReadCombinedAsync(one, two, three);
}

or

private static async Task<Task<string>> DoItTask()
{
    string one = await ReadOne();
    string two = await ReadOne();
    string three = await ReadOne();

    return ReadCombinedAsync(one, two, three);
}

The latter actually returns a Task<string> as the result of the async Task.

Note: I don't recommend doing this, it will be very confusing for other developers as this is unexpected. Tasks should be awaited whenever possible. If you return a Task, you'll lose parts of the call stack in the stack trace when an exception occurs and it makes debugging quite nasty if you do this in many places, especially when wrapping Tasks in Tasks like this.

Julian
  • 5,290
  • 1
  • 17
  • 40
  • I think @MichaelLiu comment above summed it up well, having an await within the method requires the two solutions you provided. – David Thielen Jun 03 '23 at 21:13
  • 2
    The `DoSomething()` method _does_ return a `Task`. Methods can't just decide to return something different from their signature. – StriplingWarrior Jun 03 '23 at 21:16
  • @StriplingWarrior I've removed the part with the `DoSomethingAsyc()` example, because it only leads to confusion. You're right about that it *does* return a Task - after the compiler removed the `async` and `await` syntactic sugar. – Julian Jun 03 '23 at 22:01
  • 4
    The two options you mention at the end still need to be declared `async` because they are `await`ing the results of ReadOne. – StriplingWarrior Jun 03 '23 at 22:24
  • Ah yes, I forgot to add the `async` keywords after copying the code. My bad. Fixed that. – Julian Jun 04 '23 at 06:23
  • The keyword availability isn't really tied to the TPL but is more combination of a grammatical rule of the language itself and the requirement to use bespoke return types. – Aluan Haddad Jun 07 '23 at 00:50
  • @AluanHaddad That is true, but it doesn't render my statement incorrect. When you use Tasks (so TPL) and you want to `await` them, you have to use an `async` method. – Julian Jun 07 '23 at 06:15
  • @Julian I'm not suggesting that it does. There's plenty of good information in this answer – Aluan Haddad Jun 07 '23 at 06:17