In a very early post, I stated:
An ‘async’ method is not free. The compiler does some complex code generation steps to morph your method into a proper asynchronous state machine with all the associated exception handling logic, management of continuation callbacks, etc.
Let’s explore this in a bit more detail. First, we will start with a very basic console application:
namespace ConsoleApplication1 { using System; using System.Threading.Tasks; internal sealed class Program { private static void Main(string[] args) { WaitOneSecondAwaitAsync().Wait(); WaitOneSecondPassThroughAsync().Wait(); } private static Task WaitOneSecondPassThroughAsync() { return Task.Delay(TimeSpan.FromSeconds(1.0d)); } private static async Task WaitOneSecondAwaitAsync() { await Task.Delay(TimeSpan.FromSeconds(1.0d)); } } }
Now let’s use ILSpy to decompile the app so we can see all the generated code. To do this, make sure to uncheck “Decompile async methods (async/await)” in the Decompiler tab under View -> Options. The first thing to note is how much additional code the single line await
call actually expands to in the WaitOneSecondAwaitAsync
method:
[DebuggerStepThrough, AsyncStateMachine(typeof(Program.<WaitOneSecondAwaitAsync>d__0))] private static Task WaitOneSecondAwaitAsync() { Program.<WaitOneSecondAwaitAsync>d__0 <WaitOneSecondAwaitAsync>d__; <WaitOneSecondAwaitAsync>d__.<>t__builder = AsyncTaskMethodBuilder.Create(); <WaitOneSecondAwaitAsync>d__.<>1__state = -1; AsyncTaskMethodBuilder <>t__builder = <WaitOneSecondAwaitAsync>d__.<>t__builder; <>t__builder.Start<Program.<WaitOneSecondAwaitAsync>d__0>(ref <WaitOneSecondAwaitAsync>d__); return <WaitOneSecondAwaitAsync>d__.<>t__builder.Task; }
Now locate the struct
inside Program
named <WaitOneSecondAwaitAsync>d__0
and look at its definition:
using ConsoleApplication1; using System; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading.Tasks; [CompilerGenerated] [StructLayout(LayoutKind.Auto)] private struct <WaitOneSecondAwaitAsync>d__0 : IAsyncStateMachine { public int <>1__state; public AsyncTaskMethodBuilder <>t__builder; private TaskAwaiter <>u__$awaiter1; private object <>t__stack; void IAsyncStateMachine.MoveNext() { try { int num = this.<>1__state; TaskAwaiter taskAwaiter; if (num != 0) { taskAwaiter = Task.Delay(TimeSpan.FromSeconds(1.0)).GetAwaiter(); if (!taskAwaiter.IsCompleted) { this.<>1__state = 0; this.<>u__$awaiter1 = taskAwaiter; this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter, Program.<WaitOneSecondAwaitAsync>d__0>(ref taskAwaiter, ref this); return; } } else { taskAwaiter = this.<>u__$awaiter1; this.<>u__$awaiter1 = default(TaskAwaiter); this.<>1__state = -1; } taskAwaiter.GetResult(); taskAwaiter = default(TaskAwaiter); } catch (Exception exception) { this.<>1__state = -2; this.<>t__builder.SetException(exception); return; } this.<>1__state = -2; this.<>t__builder.SetResult(); } [DebuggerHidden] void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine param0) { this.<>t__builder.SetStateMachine(param0); } }
All this just to await
one Task
! Hopefully it is clearer now why you should simply pass through when it is convenient to do so.
One more thing about async
methods that may be surprising in some circumstances: they will never throw synchronously. Consider this code:
private static async Task AppendLineAsync(string path, string line) { if (path == null) { throw new ArgumentNullException("path"); } using (FileStream stream = GetAsyncStream(path)) { stream.Seek(0, SeekOrigin.End); byte[] buffer = Encoding.UTF8.GetBytes(line + Environment.NewLine); await stream.WriteAsync(buffer, 0, buffer.Length); } }
If you call this method with a null path
you will get this exception but only after await
ing:
System.AggregateException: One or more errors occurred. ---> System.ArgumentNullException: Value cannot be null. Parameter name: path at ConsoleApplication1.Program.<AppendLineAsync>d__0.MoveNext() [ . . . ]
If you prefer “throw on start” behavior, you need to do all the validation upfront in a non-async
method and then pass through to an inner async
method where all the actual logic lives. The pattern looks something like this:
private static Task AppendLineAsync(string path, string line) { if (path == null) { throw new ArgumentNullException("path"); } return AppendLineInnerAsync(path, line); } private static async Task AppendLineInnerAsync(string path, string line) { using (FileStream stream = GetAsyncStream(path)) { stream.Seek(0, SeekOrigin.End); byte[] buffer = Encoding.UTF8.GetBytes(line + Environment.NewLine); await stream.WriteAsync(buffer, 0, buffer.Length); } }
Now the null argument case will throw directly:
System.ArgumentNullException: Value cannot be null. Parameter name: path at ConsoleApplication1.Program.Main(String[] args)
I suck at benchmarks. But I made a test and saw passthrough is nearly 2x faster.
https://gist.github.com/ahmetalpbalkan/81cccd38cc37616de506
output with 1M iterations:
passthrough 4073.4583 ms.
await 7424.285 ms.
Thanks for reading! Interesting benchmark! I built upon yours and added a few optimizations to more directly compare the two patterns and came up with this:
https://gist.github.com/brian-dot-net/9519881
Compiled with Release mode, ran a few times and got a fairly consistent result (no more than a few percent variance):
Thanks Brian and Ahmet.
14 times is quite a difference. So we should avoid method like –
public async Task GetFooNameAsync()
{
var foo = await GetFooAsync();
return foo.Name;
}
Since in this case the calling method can get the name if we return Foo right?
Yes, though keep in mind this is a micro-benchmark and in the real world there are many other sources of overhead that might dwarf this cost in practice.
Pingback: ContinueWith slowly | WriteAsync .NET