Demystify Async and Await (Part 2 of 2)

2016/03/29

no comments

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?

Code Snippet
  1. private async Task ExecAsync()
  2. {
  3.     // Do nothing yet return Task
  4. }

 

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:

Code Snippet
  1. private static async Task ExecAsync()
  2. {
  3.     await DoSomethingAsync();
  4.     WriteThreadInfo("ExecAsync");
  5. }

 

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:

Code Snippet
  1. private static async Task ExecAsync()
  2. {
  3.     await DoSomethingAsync();
  4.     WriteThreadInfo("ExecAsync");
  5. }
  6.  
  7. private static Task DoSomethingAsync()
  8. {            
  9.     return Task.CompletedTask; // .NET 6 API use Task.FromResult(1) on previous versions
  10. }

 

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:

Code Snippet
  1. private async void Exec()
  2. {
  3.     for (int i = 0; i < 10; i++)
  4.     {
  5.         Console.WriteLine(i);
  6.         await Task.Delay(10);
  7.     }
  8. }

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 :

Code Snippet
  1. private async Task Exec()
  2. {
  3.     for (int i = 0; i < 10; i++)
  4.     {
  5.         Console.WriteLine(i);
  6.         await Task.Delay(10);
  7.     }
  8.     // we return nothing explicitly
  9. }

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:

Code Snippet
  1. private async Task<int> Exec()
  2. {
  3.     for (int i = 0; i < 10; i++)
  4.     {
  5.         Console.WriteLine(i);
  6.         await Task.Delay(10);
  7.     }
  8.     // we return int which re-write into Task<int>
  9.     return 42;
  10. }

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:

Code Snippet
  1. private static async Task ExecAsync()
  2. {
  3.     Console.WriteLine("First synchronous line of execution");
  4.     await Task.Delay(100); // end of the first line of execution
  5.     Console.WriteLine("Second asynchronous line of execution");
  6. }

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:

Code Snippet
  1. private static Task ExecAsync()
  2. {
  3.     Console.WriteLine("First synchronous line of execution");
  4.     Task firstLineOfExecution = Task.Delay(100);
  5.     Task firstsecondLineOfExecution =
  6.         firstLineOfExecution.ContinueWith(c =>
  7.             Console.WriteLine("Second asynchronous line of execution"));
  8.     return firstsecondLineOfExecution;
  9. }

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.

Code Snippet
  1. private async Task ExecAsync()
  2. {
  3.     Console.WriteLine("First task run synchronously");
  4.     await Task.Delay(100);
  5.     Console.WriteLine(@"This part will be
  6.                         may schedule on Thread Pool");
  7.     await Task.Run(() => Console.WriteLine("asynchronously by nature"));
  8.     Console.WriteLine(@"may schedule on Thread Pool");
  9. }

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.

Code Snippet
  1. public partial class MainWindow : Window, ICommand
  2. {
  3.     public event EventHandler CanExecuteChanged;
  4.  
  5.     public bool CanExecute(object parameter) => true; // C# 6 syntax
  6.  
  7.     public async void Execute(object parameter)
  8.     {
  9.         using (var http = new HttpClient())
  10.         {
  11.             string data =
  12.                 await http.GetStringAsync("Http://somewhere/….");
  13.             Data.Add(data); // safe assignment from UI thread
  14.         }
  15.     }
  16.  
  17.     public ObservableCollection<string> Data { get; } =
  18.         new ObservableCollection<string>(); // C# 6 syntax
  19. }

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:

Code Snippet
  1. public async void Execute(object parameter)
  2. {
  3.     using (var http = new HttpClient())
  4.     {
  5.         string item = await HandleNonUIAspectsAsync();
  6.         Data.Add(item); // safe assignment from UI thread
  7.     }
  8. }
  9.  
  10. public async Task<string> HandleNonUIAspectsAsync()
  11. {
  12.     using (var http = new HttpClient())
  13.     {
  14.         string data =
  15.             await http.GetStringAsync("Http://somewhere/….")
  16.                       .ConfigureAwait(false); // don't continue on UI thread
  17.         using (var fs = new FileStream("Log.txt",
  18.             FileMode.Create, FileAccess.Write, FileShare.None, 4096,
  19.             FileOptions.Asynchronous)) // this overload will use IOCP
  20.         using(var w = new StreamWriter(fs))
  21.         {
  22.             await w.WriteAsync(data); // single ConfigureAwait within
  23.                                         // the method is more than enough
  24.         }
  25.         return data;             
  26.     }
  27. }

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.

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*