Demystify Async and Await (Part 2 of 2)
This post is the second post on this series.
I’m strongly recommend to read the first post before this one.
On this post I will demystify async and await.
So many developers are having wrong understanding of the feature
all over the industry. This lead to bad practice and confusion.
On this post I will try to clarify the true nature of async and await.
Misconception of async and await
before I’ll explain what async and await really are, I’ll start with
what it doesn’t.
I will start with a quiz.
Does the method [ExecAsync] (on the following code snippet) is running asynchronously?
- private async Task ExecAsync()
- {
- // Do nothing yet return Task
- }
The answer is not.
It may seem confusion because ExecAsync return Task, but as you know
from the fist post (on this series), that Task is data structure which represent the
execution context (synchronous or asynchronous).
Nothing on the code above cause asynchronous execution.
When realizing this, people often think that the await
introduce asynchronous execution.
Take a look on the following code snippet and try to figure out if it run synchronously or asynchronously:
- private static async Task ExecAsync()
- {
- await DoSomethingAsync();
- WriteThreadInfo("ExecAsync");
- }
The answer is, you couldn’t know.
It’s depend whether DoSomethingAsync is running synchronously or asynchronously.
the await itself don’t introduce concurrency.
The following code will execute synchronously:
- private static async Task ExecAsync()
- {
- await DoSomethingAsync();
- WriteThreadInfo("ExecAsync");
- }
- private static Task DoSomethingAsync()
- {
- return Task.CompletedTask; // .NET 6 API use Task.FromResult(1) on previous versions
- }
What is the true meaning of async and await?
As you saw on the previous section not async nor await produce concurrency.
So the question is what do it really do and is it related to parallel execution.
The most fundamental understanding of async and await is to know that it’s
all about the compiler.
async and await are not a library, it’s compile-time feature.
The compiler re-writing your code into complex (yet efficient) state machine.
You don’t really care about the exact structure of the re-writes but you should remember
that your code isn’t really what you’re seeing.
When the compiler do its magic, it’s adding a few optimization, for example
it won’t schedule anything on new thread when it completes synchronously.
How can you reason about async and await
async is mainly a marker which tell us that the method
may run asynchronously. this marker is important for us human, when we do
code review, we could miss the fact that the method may run asynchronously.
For example the following code may seem to execute synchronously at first glance:
- private async void Exec()
- {
- for (int i = 0; i < 10; i++)
- {
- Console.WriteLine(i);
- await Task.Delay(10);
- }
- }
We can easily miss line 6 and consider the method synchronic.
Except from being a marker async allow the method signature to return Task
without returning Task explicitly from our code.
No Task returning explicitly on the following code snippet :
- private async Task Exec()
- {
- for (int i = 0; i < 10; i++)
- {
- Console.WriteLine(i);
- await Task.Delay(10);
- }
- // we return nothing explicitly
- }
The compiler will return Task when it re-write the code.
Another sample is when you do return something, async wrap it with Task<T>.
As shown on the following code snippet:
- private async Task<int> Exec()
- {
- for (int i = 0; i < 10; i++)
- {
- Console.WriteLine(i);
- await Task.Delay(10);
- }
- // we return int which re-write into Task<int>
- return 42;
- }
I will explain latter what the Task represent.
Now it’s time to answer what the await is for.
The await signal the compiler where to break the original method into
different line of execution. As I mentioned earlier, the compiler is re-writing our code
into state machine. Each state of the state machine represent single line of execution.
Concurrency may introduce when moving from one line of execution to the other.
In general each line of execution is continuation of the previous line of execution.
For example the following code snippet has 2 line of execution:
- private static async Task ExecAsync()
- {
- Console.WriteLine("First synchronous line of execution");
- await Task.Delay(100); // end of the first line of execution
- Console.WriteLine("Second asynchronous line of execution");
- }
It important to mention that what cause concurrency is the fact that the first
line of execution ends with truly asynchronous call (Task.Delay).
The compiler will break this method into 2 parts.
The first part (lines 3,4) and the second part (line 5).
The execution of line 5 will be schedule by the TaskScheduler as continuation of
the completion of the first part (line of execution).
If I will replace Task.Delay with Task.CompletedTask the second line of execution will run
on the same thread as the first line of execution (synchronously).
You can achieve the same functionality with the old TPL by using ContinueWith.
See the following code snippet:
- private static Task ExecAsync()
- {
- Console.WriteLine("First synchronous line of execution");
- Task firstLineOfExecution = Task.Delay(100);
- Task firstsecondLineOfExecution =
- firstLineOfExecution.ContinueWith(c =>
- Console.WriteLine("Second asynchronous line of execution"));
- return firstsecondLineOfExecution;
- }
async and await is using the same semantic shown on the above snippet.
This mean that the Task returns from the following snippet represent the completion
of the entire logical call, even though it’s broken into multiple concurrent calls (by the compiler) and each
part may run on different thread.
The following snippet show asynchronous method that represent single logical operation
which broken down into 3 technical execution lines.
- private async Task ExecAsync()
- {
- Console.WriteLine("First task run synchronously");
- await Task.Delay(100);
- Console.WriteLine(@"This part will be
- may schedule on Thread Pool");
- await Task.Run(() => Console.WriteLine("asynchronously by nature"));
- Console.WriteLine(@"may schedule on Thread Pool");
- }
Even though this method is broken into 3 different execution lines (on line 4 and 7),
it represent single logical execution path.
The Task which returns from the method represent the logical path execution.
Therefore it will be completed only when the code reach line 9.
Compiler Re-write benefits
Compared with ContinueWith, async and await bringing more elegant API and better performance.
The compiler is doing some optimization which may result with less memory allocation and faster execution.
Another benefit of the compiler re-write is exception handling.
When come to concurrent code, it is very easy to forget some execution path and leave code unhandled.
Because async and await is actually compile time feature the compiler take the catch block and re-write it
for each execution path. This way no code is left unhandled.
More about it and more can be read on this post.
TaskScheduler will schedule each execution line
As I said earlier, each await break your code into execution line,
but what schedule each execution line?
The responsibility of the execution rely on TaskScheduler,
but which TaskScheduler is taken?
TaskScheduler.Current is the scheduler which is used.
By default TaskScheduler.Current equals to TaskScheduler.Default.
The default scheduler is taking smart decision base on the execution context.
In case that the method originally called from Synchronization Context (from example from UI),
it will use the Synchronization Context for schedule the next execution line (which means that on the UI case
the code after the await will re-schedule on the UI thread).
For example, the following code is totally safe.
- public partial class MainWindow : Window, ICommand
- {
- public event EventHandler CanExecuteChanged;
- public bool CanExecute(object parameter) => true; // C# 6 syntax
- public async void Execute(object parameter)
- {
- using (var http = new HttpClient())
- {
- string data =
- await http.GetStringAsync("Http://somewhere/….");
- Data.Add(data); // safe assignment from UI thread
- }
- }
- public ObservableCollection<string> Data { get; } =
- new ObservableCollection<string>(); // C# 6 syntax
- }
Line 13 would have thrown exception unless the code execute on the UI thread.
Because the Execute method start on the UI thread, line 13 will be schedule on
the UI thread by the default TaskScheduler.
Sometimes you want to avoid this behavior, because you handle non UI code after the await
(and you don’t want to steal time from the UI thread for non-UI operations).
In order to alter this behavior you have to use ConfigureAwait(false).
ConfigureAwait(false) disable the behavior of the Synchronization Context which mean
that the default TaskScheduler will schedule the execution line on the Thread Pool.
ConfigureAwait(false) effect ends when the method ends. You can use it
within sub method in order to limit its effect.
However when it set to false, nothing will get you back to the Synchronization context
until the method ends.
The following code snippet show how can you limit its effect:
- public async void Execute(object parameter)
- {
- using (var http = new HttpClient())
- {
- string item = await HandleNonUIAspectsAsync();
- Data.Add(item); // safe assignment from UI thread
- }
- }
- public async Task<string> HandleNonUIAspectsAsync()
- {
- using (var http = new HttpClient())
- {
- string data =
- await http.GetStringAsync("Http://somewhere/….")
- .ConfigureAwait(false); // don't continue on UI thread
- using (var fs = new FileStream("Log.txt",
- FileMode.Create, FileAccess.Write, FileShare.None, 4096,
- FileOptions.Asynchronous)) // this overload will use IOCP
- using(var w = new StreamWriter(fs))
- {
- await w.WriteAsync(data); // single ConfigureAwait within
- // the method is more than enough
- }
- return data;
- }
- }
line 16 cancel the Synchronization Context behavior, which mean that from
line 17 the executions line won’t schedule on the UI thread.
This still affect the execution line starts at line 24, even though WriteAsync don’t use ConfigureAwait(false).
Line 6 is still scheduling on the UI thread because the effect of ConfigureAwait(false) is limited to the
method it define in.
Last point worth to mention, is about doing asynchronous work with Files.
The right way to open file for asynchronous operations is to use the
specify FileOptions.Asynchronous as shown at line 19.
This way the operation will use IOCP.
Summary
async and await is powerful concept which often don’t understood correctly.
I hope that this post help to clarify the true nature of it and lead to better code.