Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does async-await not block?

Tags:

c#

async-await

I gather that the async methods are good for IO work because they don't block the thread whilst they're being awaited, but how is this actually possible? I assume something has to be listening to trigger the task to complete, so does this mean that the blocking is just moved somewhere else?

like image 389
NickL Avatar asked Dec 13 '13 18:12

NickL


People also ask

How is async await non-blocking?

async lets you use await . That's (almost) all it does (It also wraps your result in a promise). Together they make non-blocking code read like simpler blocking code. They don't unblock code.

Does async await block the thread?

The await operator doesn't block the thread that evaluates the async method. When the await operator suspends the enclosing async method, the control returns to the caller of the method.

Does await stop execution?

The await expression causes async function execution to pause until a Promise is settled (that is, fulfilled or rejected), and to resume execution of the async function after fulfillment.

Does await block next line?

Short answer is yes, it blocks. The purpose of await is, to make sure you have the result available in the next line.


2 Answers

No, the blocking is not moved anywhere else. BCL methods that return awaitable types use techniques such as overlapped I/O with I/O completion ports for a fully asynchronous experience.

I have a recent blog post that describes how this works all the way down to the physical device and back.

like image 59
Stephen Cleary Avatar answered Sep 22 '22 22:09

Stephen Cleary


Async-await is actually re-writing your code for you. What it does is use a Task Continuation and puts that continuation back on to the Synchronization context that was current when the continuation was created.

So the following function

public async Task Example()
{
    Foo();
    string barResult = await BarAsync();
    Baz(barResult);
}

Gets turned in to something like (but not exactly) this

public Task Example()
{
    Foo();
    var syncContext = SyncronizationContext.Current;
    return BarAsync().ContinueWith((continuation) =>
                    {
                        Action postback = () => 
                        {
                            string barResult = continuation.Result();
                            Baz(barResult)
                        }

                        if(syncContext != null)
                            syncContext.Post(postback, null);
                        else
                            Task.Run(postback);
                    });
}

Now its actually a lot more complicated than that, but that is the basic gist of it.


What really is happening is it it calls the function GetAwaiter() if it exists and does something more like this

public Task Example()
{
    Foo();
    var task = BarAsync();
    var awaiter = task.GetAwaiter();

    Action postback = () => 
    {
         string barResult = awaiter.GetResult();
         Baz(barResult)
    }


    if(awaiter.IsCompleted)
        postback();
    else
    {
        var castAwaiter = awaiter as ICriticalNotifyCompletion;
        if(castAwaiter != null)
        {
            castAwaiter.UnsafeOnCompleted(postback);
        }
        else
        {
            var context = SynchronizationContext.Current;

            if (context == null)
                context = new SynchronizationContext();

            var contextCopy = context.CreateCopy();

            awaiter.OnCompleted(() => contextCopy.Post(postback, null));
        }
    }
    return task;
}

This is still not exactly what happens, but The important thing to take away is if awaiter.IsCompleted is true, it will run the postback code synchronously instead of just returning right away.

The cool thing is, you don't need to await on a Task, you can await anything as long as it has a function called GetAwaiter() and the returned object can fulfill the following signature

public class MyAwaiter<TResult> : INotifyCompletion
{
    public bool IsCompleted { get { ... } }
    public void OnCompleted(Action continuation) { ... }
    public TResult GetResult() { ... }
}
//or
public class MyAwaiter : INotifyCompletion
{
    public bool IsCompleted { get { ... } }
    public void OnCompleted(Action continuation) { ... }
    public void GetResult() { ... }
}

On the continuing adventure on making my wrong answer even more wrong, here is the actual decompiled code the compiler turns my example function in to.

[DebuggerStepThrough, AsyncStateMachine(typeof(Form1.<Example>d__0))]
public Task Example()
{
    Form1.<Example>d__0 <Example>d__;
    <Example>d__.<>4__this = this;
    <Example>d__.<>t__builder = AsyncTaskMethodBuilder.Create();
    <Example>d__.<>1__state = -1;
    AsyncTaskMethodBuilder <>t__builder = <Example>d__.<>t__builder;
    <>t__builder.Start<Form1.<Example>d__0>(ref <Example>d__);
    return <Example>d__.<>t__builder.Task;
}

Now if you look through there you will see there is no reference to Foo(), BarAsync(), or Baz(barResult) this is because when you use async the compiler actually turns your function in to a state machine based on the IAsyncStateMachine interface. If we go look, the compiler generated a new struct called <Example>d__0

[CompilerGenerated]
[StructLayout(LayoutKind.Auto)]
private struct <Example>d__0 : IAsyncStateMachine
{
    public int <>1__state;
    public AsyncTaskMethodBuilder <>t__builder;
    public Form1 <>4__this;
    public string <barResult>5__1;
    private TaskAwaiter<string> <>u__$awaiter2;
    private object <>t__stack;
    void IAsyncStateMachine.MoveNext()
    {
        try
        {
            int num = this.<>1__state;
            if (num != -3)
            {
                TaskAwaiter<string> taskAwaiter;
                if (num != 0)
                {
                    this.<>4__this.Foo();
                    taskAwaiter = this.<>4__this.BarAsync().GetAwaiter();
                    if (!taskAwaiter.IsCompleted)
                    {
                        this.<>1__state = 0;
                        this.<>u__$awaiter2 = taskAwaiter;
                        this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<string>, Form1.<Example>d__0>(ref taskAwaiter, ref this);
                        return;
                    }
                }
                else
                {
                    taskAwaiter = this.<>u__$awaiter2;
                    this.<>u__$awaiter2 = default(TaskAwaiter<string>);
                    this.<>1__state = -1;
                }
                string arg_92_0 = taskAwaiter.GetResult();
                taskAwaiter = default(TaskAwaiter<string>);
                string text = arg_92_0;
                this.<barResult>5__1 = text;
                this.<>4__this.Baz(this.<barResult>5__1);
            }
        }
        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);
    }
}

Thanks to the people over at ILSpy for making their tool use a library that you can extend and call from code yourself. To get the above code all I had to do was

using System.IO;
using ICSharpCode.Decompiler;
using ICSharpCode.Decompiler.Ast;
using Mono.Cecil;

namespace Sandbox_Console
{
    internal class Program
    {
        public static void Main()
        {
            AssemblyDefinition assembly = AssemblyDefinition.ReadAssembly(@"C:\Code\Sandbox Form\SandboxForm\bin\Debug\SandboxForm.exe");
            var context = new DecompilerContext(assembly.MainModule);
            context.Settings.AsyncAwait = false; //If you don't do this it will show the original code with the "await" keyword and hide the state machine.
            AstBuilder decompiler = new AstBuilder(context);
            decompiler.AddAssembly(assembly);

            using (var output = new StreamWriter("Output.cs"))
            {
                decompiler.GenerateCode(new PlainTextOutput(output));
            }
        }
    }
}
like image 42
Scott Chamberlain Avatar answered Sep 21 '22 22:09

Scott Chamberlain