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?
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.
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.
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.
Short answer is yes, it blocks. The purpose of await is, to make sure you have the result available in the next line.
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.
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));
}
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With