14

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?

NickL
  • 1,870
  • 2
  • 15
  • 35

2 Answers2

20

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.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
12

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));
            }
        }
    }
}
Community
  • 1
  • 1
Scott Chamberlain
  • 124,994
  • 33
  • 282
  • 431
  • "Okay, so this just means the blocking is done in BarAsync()." I know you're right, but your answer doesn't explain why, whereas Stephen Cleary's does. –  Dec 13 '13 at 18:52
  • @m59 None of the things that you just put in code blocks are actual blocks of code... – Servy Dec 13 '13 at 18:52
  • @Servy oops, apologies. I thought they were referring to a function. It still seems like they should stand out...is there something better to highlight that with? – m59 Dec 13 '13 at 18:53
  • @hvd no, no blocking at all happens. The task from `BarAsync()` is immedatly returned to the calling function. When BarAsync completes it does the `Action` delegate `postback` either on the Syncronization context if there was one (The message loop for Windows Forms, the dispatcher for WPF) or on to the ThreadPool if there was none. – Scott Chamberlain Dec 13 '13 at 18:55
  • @ScottChamberlain Yes, I know, but I've had similar trouble understanding this as the OP. I could see that `Example` does not block, but I figured `BarAsync` either had to block, or call yet another async function. At the low level, *something* had to block, surely? And it's precisely that misunderstanding that I think Stephen Cleary's answer addresses. –  Dec 13 '13 at 18:57
  • 1
    There is no requirement that `BarAsync` uses blocking internally, it could just be something that takes a long time to process that you put on the background thread. Blocking involves stopping your code and waiting for a external source, you can do that (for example the IO Completion ports Stepen talks about) but it is not a requirement. – Scott Chamberlain Dec 13 '13 at 19:00
  • 1
    @ScottChamberlain For the purposes of this question, I think blocking a background thread is still blocking, even if the current thread can continue. From the question: "so does this mean that the blocking is just moved somewhere else?" For example, a background thread. (But I think this can become a pointless discussion very quickly. Like I said, I do think your answer is entirely correct. If I asked the question, it would not help me understand. But perhaps it does help the OP understand. :) ) –  Dec 13 '13 at 19:02
  • 2
    @hvd I did not read the question closely enough the first time, I agree with you 100%. I won't delete my answer because I think it contains useful information but Stephen defiantly is the "correct" answer for this one. I answered the title, not the question :( – Scott Chamberlain Dec 13 '13 at 19:05
  • @hvd Scott wasn't referring to blocking another BG thread though, merely performing CPU bound work in another thread that, at some point, results in an asynchronous operation being marked as completed. Yes, at some point a thread is always doing some work at that point (unlike Stephen's example, in which there is no spoon, I mean thread) but there isn't a thread sitting around twiddling it's thumbs doing nothing, i.e. blocking, which does make the code properly asynchronous. – Servy Dec 13 '13 at 19:10
  • @Servy Heh, you're right, I think I ended up confusing myself with these comments. –  Dec 13 '13 at 19:19