Understanding the Async Await API - part 1 (.NET)

 The new Async-await feature in the .NET 4.5 framework is great, it allows you to free up the main thread without breaking the usual code flow or losing readability, but it takes a bit of thinking to understand it fully. Here is a walkthrough, presenting answers to some of the common questions and explaining the misconceptions I encountered when I first used the API for the Await feature, and how it relates to the previously released Task Parallel Library (TPL).

 

A real world example of await looks like this:

    string html;
    using (WebClient client = new WebClient())
    {
       html = await client.DownloadStringTaskAsync(@"http://someURL/go.html");
    }
    html.Split("<> ".ToCharArray());

 

 

It looks very intuitive, but as you run it you start thinking:

When using the ‘await’ keyword, it seems as though .Net has a “magical” mechanism that enables code currently being run on the main thread to move to another thread.

While this first impression is somewhat inaccurate, it still brings you to the question: what are the consequences?

 

What is the flow when using Await?

Some confusion comes from the actual word await – does the flow wait for an operation to complete?

In reality, it actually does, but without locking the thread. Instead, the operation is suspended, its data is saved and a scheduler is responsible for resuming the operation when awaited task is done.

 

There are actually several questions that need to be asked:

  • What is actually happening when I change the flow?
  • Will this cause thread safety issues?
  • How will the function’s local variables be kept?
  • When will the results be returned?
  • What thread will do the work when it returns?
  • Will this cause operations which are bound to the main thread to run in a different thread?

 

Let’s look at this simple example:

       async void Function()
        {
            Console.Out.WriteLine("before");
            await Task.Run(() => Console.Out.WriteLine("between"));
            Console.Out.WriteLine("after");
        }

 

 

You can think of the “await” process like this:

  1. Call "before" code
  2. Call await code as a task
  3. Store "after" code in a continuation task
  4. Release the thread to perform other operations

 

This is not a very accurate description (a better one is shown below), but it is close. The important thing to remember is that the code flow is sequential by nature, since the “after” code usually relies on result values generated by the awaited code and it will stay that way after inserting the await keyword.

Therefore, after using await, the code of the function will not become parallel, it will only become more ready to relinquish control over the thread, thus allowing other actions to be performed in parallel to it, while using the same thread.

 

Conclusion:

Await (by itself) != parallel function flow

 

This serial code flow has two main consequences:

  1. Just adding await to a function will not make it go faster (not when using multi cores, anyway)
  2. Adding await to a function will not damage code flow. (exception handling included)

 

Two minor points can be made here:

  1. You may see a small performance boost if you go from using threads to using IO completion.
  2. Since other flows can be run in parallel to yours, you do need to think about locking resources if other flows can change them.

 

To create parallelism you need to invoke parallel tasks intentionally. This can be done by using the Task.Run method or one of the older TPL APIs.

 

Now that we got our facts right, we can go over the way the mechanism actually works (as promised above), without getting confused by the details.

 

The actual flow of the await API:

  1. Compile time:
  • A structure called StateMachine is generated which contains fields to save the local state of the function.
  • A MoveNext function is created in the machine class and holds the entire function code.
  • The code is fragmented by await calls into several cases (machine states).
  • A calling code which creates and initializes this machine replaces our async function code.

 

  1. Runtime:
  • A task is created to run the machine code.
  • Local variables are “lifted” into the state machine as fields.
  • The Code is run until await
  • An awaited function's task is run.
  • Machine state is set to next state so next code fragment will run on wake-up.
  • A wake-up event is scheduled.
  • A MoveNext function returns (and the thread is released to perform other activities).

 

  1. When the wakeup call is issued by the OS:
  • The thread which handles the Await continuation is called.
  • A CurrentSyncContext is used to pick the correct thread on which to run.
  • The Next code segment is run since next state was set before yielding control again.
  • Another await is scheduled, [etc.]

 

How about our questions?

  • What is actually happening when I change the flow?
    • The flow stays serial, items will be executed in the same order
  • Will this cause thread safety issues?
    • The possibility exists:
      • Interactions between flows become possible as several flows can interchange on the main thread, or run in parallel (depends on the SyncContext used)
      • The awaited action is run in a different thread, and can change data.
  • How will the function’s local variables be kept?
    • They are passed to a temporary object called the state machine.
  • When will the results be returned?
    • The result is returned when the awaited operation is done and suspended flow is resumed.
  • What thread will do the work when it returns?
    • The thread chosen to do the work will be the same thread that started it or one that closely resembles it, since it uses the same synchronization context.
  • Will this cause operations which are bound to the main thread to run in a different thread?
    • Usually not, as the context reroutes the flow back into the main thread, the only problem can be with the awaited code which is done in another thread (and should not do any main thread bound operations).

 

The Await API’s relation to TPL

TPL was made for using threads from the thread pool to create parallel flows in your code.

The Await method was made to release the main thread from its current flow in order to allow other flows to use the same thread for their operations.

While Await uses TPL internally in its implementation, and can sometimes be used in collaboration with TPL, they are different APIs, each with its own merit and usage.

 

There are some differences between these two mechanisms:

  1. Await will try to come back to the thread from which it started (or an equivalent one), while TPL uses threads from the thread pool.

The thread for the “after” code is decided by the “before” thread’s CurrentSyncContext which is different depending on the application type.

  • This behavior can be changed by calling the task.ConfigureAwait(false) method, and in TPL - passing the current sync context.
  • The difference in default behavior shows that these two APIs differ in the use cases they target. (TPL = to make work run in parallel, await = to release threads from suspension when IO actions are performed)

 

  1. Exception handling:

In TPL:

  1. An AggregateException is used to return all exceptions at once. (special handling)
  2. Unhandled exceptions are discovered and thrown by the finalizer, which is detached from regular code flow.

In Await:

  1. Always throws the first exception (handling is done by a regular catch phrase)
  2. When using the WhenAll method to await multiple tasks, the first exception will be thrown after all operations are complete.
  3. Unhandled exceptions go up the stack and end the application in the usual way.

 

An example of exception handling when using await:

  

          List<Task> tasks = new List<Task>();
            try
            {
                tasks.Add(Task.Run(() =>
                {
                    throw new Exception("something bad just happend (1)!");
 
                }));
                tasks.Add(Task.Run(() =>
                {
                    throw new Exception("something bad just happend (2)!");
 
                }));
                await Task.WhenAll(tasks.ToArray());
            }
            catch (Exception ex)
            {
                //only first exception will be caught here
                string message = ex.Message;
            }

 

The two tasks will run to completion before a single exception is caught in the catch clause.

Two exceptions will be thrown, but the first one will be caught and handled.

 

  1. When using Await: The first portion of the async function code will be run synchronously in the calling thread, When using TPL, all the code is wrapped in a task and run in a thread from the pool

 

 

Do I always need to Await an async function?

If marking a function as async (which is needed in order to use Await inside it) forces you to call Await when calling it, you would be forced to have async on all of your functions recursively. If this was the case – you would try to avoid using it altogether.

So the answer to the question above is no, but this requires some explanation:

An async function forces you to return a task<resultType> value. This is understandable since you will need a way to get the result when calling the function in an asynchronous manner.

However,

  • You can use the task.RunSynchronously() method to invoke the task in a synchronous way.
  • An async void has no return value and will not return a task. It will always start running synchronously in the calling thread.

 

Here is an example of the different usages (read the code comments):

 

  async void MainVoid()
        {
            string a;
 
            //use await to continue when task ends
            a = await FunctionAsyncWithResult();
           
            //run the task synchronously in the main thread
            Task<string> t = FunctionAsyncWithResult();
            t.RunSynchronously();
            a = t.Result;
 
            //a void will run synchronously
            FunctionAsyncVoid();
        }
 
        async Task<string> FunctionAsyncWithResult()
        {
            Console.Out.WriteLine("before");
            await Task.Run(() => Console.Out.WriteLine("between"));
            Console.Out.WriteLine("after");
            return "";
        }
        async void FunctionAsyncVoid()
        {
            Console.Out.WriteLine("before");
            await Task.Run(() => Console.Out.WriteLine("between"));
            Console.Out.WriteLine("after");
        }

 

 

 

Async-await makes life easier and greatly simplifies the code which is needed to free up main threads and improve user experience and responsiveness, but as always with parallel programming, you should know what you are doing. I hope I helped understand this API better.

I will dedicate my next post on the subject to exploring the actual state machine implementation as it is seen in decompiled code, so we can peek inside the mechanism, and find out how it ticks.

 

This post has been written by Amit Bezalel 

 

Comments
HP Expert | ‎04-20-2013 11:26 PM

A very good read.

Waiting for the second part, I love decompiled code.

Leave a Comment

We encourage you to share your comments on this post. Comments are moderated and will be reviewed
and posted as promptly as possible during regular business hours

To ensure your comment is published, be sure to follow the Community Guidelines.

Be sure to enter a unique name. You can't reuse a name that's already in use.
Be sure to enter a unique email address. You can't reuse an email address that's already in use.
Type the characters you see in the picture above.Type the words you hear.
Search
Showing results for 
Search instead for 
Do you mean 
About the Author


Follow Us
The opinions expressed above are the personal opinions of the authors, not of HP. By using this site, you accept the Terms of Use and Rules of Participation