"Async Task Then Await Task" VS "Task Then Return Task"

async Task then await Task vs Task then return task

It is almost the same (in terms of threads etc.). But for the second one (using await) a lot more overhead will be created by the compiler.

Methods declared as async and using await are converted into a state machine by the compiler. So when you hit the await, the control flow is returned to the calling method and execution of your async method is resumed after the await when the awaited Task has finished.

As there is no more code after your await, there is no need to use await anyway. Simply return the Task is enough.

Difference between returning and awaiting a Task in an async method

There are 2 practical differences:

  1. The second option will not create the state machine mecanism that allows for async-await usage. That will have a minor positive effect on performance.
  2. The exception handling would be a little different. When you mark a method as async any exceptions are stored in the returned task (both from the asynchronous part and the synchronous one) and thrown only when the task is awaited (or waited). When it's not async, the exceptions from the synchronous parts act just like in any other method.

My suggestion: Use the second one for the added performance boost but keep an eye out for exceptions and bugs.


An example that shows the difference:

public static async Task Test()
{
Task pending = Task.FromResult(true);
try
{
pending = SendAsync1();
}
catch (Exception)
{
Console.WriteLine("1-sync");
}

try
{
await pending;
}
catch (Exception)
{
Console.WriteLine("1-async");
}

pending = Task.FromResult(true);
try
{
pending = SendAsync2();
}
catch (Exception)
{
Console.WriteLine("2-sync");
}

try
{
await pending;
}
catch (Exception)
{
Console.WriteLine("2-async");
}
}

public static async Task SendAsync1()
{
throw new Exception("Sync Exception");
await Task.Delay(10);
}

public static Task SendAsync2()
{
throw new Exception("Sync Exception");
return Task.Delay(10);
}

Output:

1-async
2-sync

async Task vs return Task.Run

Task.Run will queue its delegate to the thread pool. This causes two important things:

  1. Any code in DoStuff before the first asynchronous await will run on a thread pool thread, instead of on the calling thread.
  2. The code in DoStuff will run in a thread pool context, instead of using whatever context was current from the calling thread.

Most of the time, doing asynchronous work in Task.Run is a mistake. But it is sometimes useful, say if DoStuff does some heavy computational work before it acts asynchronously, then Task.Run could be used to move that work off of a UI thread.

await Task.CompletedTask vs return

Let's look the question from the consumer-side.

If you define an interface, which imposes an operation that returns a Task then you don't say anything about how it will be calculated / executed (so, there is no async access modifier in the method signature). It is an implementation detail.

Interface

public interface ITest
{
Task Test();
Task<bool> IsTest();
}

So, it is up to you how you implement the interface.

You can do it in a synchronous way, when there won't be any AsyncStateMachine generated because of the absence of the async keyword.

Implementation #1

public class TestImpl : ITest
{
public Task Test()
{
return Task.CompletedTask;
}

public Task<bool> IsTest()
{
return Task.FromResult(true);
}
}

Or you can try to implement it in an asynchronous way but without await operators. Here you will receive CS1998 warnings.

Implementation #2

public class TestImpl : ITest
{
public async Task Test()
{
return;
}

public async Task<bool> IsTest()
{
return true;
}
}

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

In other words this implementation does not define a state machine. An async method is divided into different states based on the await keywords:

  • before_the_await_1,
  • after_the_await_1_but_before_the_await_2
  • after_the_await_2_but_before_the_await_3
  • ...

If you haven't got any await then you would have a single state, which would run in sync (there is no need to preserve state, execute async operation and then call MoveNext()).


Or you can try to implement it in an asynchronous way with await operators.

Implementation #3

public class TestImpl : ITest
{
public async Task Test()
{
await Task.CompletedTask;
}

public async Task<bool> IsTest()
{
return await Task.FromResult(true);
}
}

In this case there will be an async state machine but it won't be allocated on the heap. By default it is a struct and if it finishes in sync then we don't need to allocate them on the heap to extend its life out of the scope of the method.

For further information please read these articles:

  • Async/await in .NET Core 3.x
  • Async ValueTask Pooling in .NET 5
  • Dissecting the async methods in C#

Await vs Task.Result in an Async Method

await asynchronously unwraps the result of your task, whereas just using Result would block until the task had completed.

See this explanantion from Jon Skeet.



Related Topics



Leave a reply



Submit