Skip to content

Instantly share code, notes, and snippets.

@71
Last active January 25, 2023 20:34
Show Gist options
  • Save 71/417f898951debd8075c07a51d229d39b to your computer and use it in GitHub Desktop.
Save 71/417f898951debd8075c07a51d229d39b to your computer and use it in GitHub Desktop.
A file I used to understand how async / await works behind-the-scenes in C#.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
namespace Tests
{
/// <summary>
/// Shows an example of code generated by the compiler for an <see langword="async"/> method.
/// </summary>
public sealed class ExampleAsync
{
#region Example method
/// <summary>
/// Asynchronously returns the answer to Life, the Universe, and
/// Everything. Fortunately, this answer is already known, and all
/// this method does is return a cached version of it,
/// after complating it a few moments.
/// </summary>
///
/// <remarks>
/// This method will be compiled to an entirely different method by the
/// compiler, and an <see cref="IAsyncStateMachine"/> will be generated
/// for it.
/// The methods below show what the result looks like.
/// </remarks>
public async Task<int> GetAnswerAsync(int contemplationTime)
{
await Task.Delay(contemplationTime);
if (contemplationTime < 1000)
return await GetAnswerAsync(1000);
return 42;
}
#endregion
[CompilerGenerated]
private Task<int> GetAnswerAsync_CompilerGenerated(int contemplationTime)
{
// This method will simply:
// - Create the generated class (ExampleStateMachine)
// - Initialize it with local variables, such as "this", and the
// "contemplationTime" parameter
// - Set its initial state (-1)
// - Initialize its AsyncTaskMethodBuilder (whose generic argument
// is the same as the return type)
// - Start the builder
// - Return the task created by the builder
ExampleStateMachine asm = new ExampleStateMachine
{
@this = this,
comtemplationTime = contemplationTime,
builder = AsyncTaskMethodBuilder<int>.Create(),
state = -1
};
AsyncTaskMethodBuilder<int> taskBuilder = asm.builder;
taskBuilder.Start(ref asm);
return asm.builder.Task;
}
#region Actual ASM implementation
/// <summary>
/// Example state machine written by a human to demonstrate how
/// the compiler generates <see cref="IAsyncStateMachine"/>s.
/// </summary>
[CompilerGenerated]
public sealed class ExampleStateMachine : IAsyncStateMachine
{
// Typically, fields will have different names:
// - Variables and parameters will keep the same name
// - @this, builder, and state will be prefixed with an identifier, ie. "<>__1builder"
public ExampleAsync @this;
public int comtemplationTime;
public AsyncTaskMethodBuilder<int> builder;
public int state;
// The private fields can be:
// - The result of the awaiters used everytime an "await ..." call is made in the method body
// - Temporary variables used in chain calls
private TaskAwaiter awaiter1;
private TaskAwaiter<int> awaiter2;
private int result1;
//
void IAsyncStateMachine.MoveNext()
{
// This method does *all the job*.
// It uses an integer named "state" to know where it is in the method,
// and if / else / goto statements to go back to a previous state.
// That way, the method can return and be called back,
// whilst keeping the same state.
// Here, state can mean a few things:
// [-2]: The result of the method is computed, or it has thrown;
// we can really return now, and never come back.
// [-1]: Start "await Task.Delay(contemplationTime)", and:
// - If it completed instantly, or if it done, keep going.
// - If it hasn't completed, wait till it ends, and return.
// [ 0]: Wait till "await Task.Delay(contemplationTime)" is done.
// [ 1]: Wait till "await GetAnswerAsync(1000)" is done, and returns something.
// Represents a local variable of the state (faster access)
int locState = this.state;
// Represents the return value of the whole method
int returnValue;
try
{
TaskAwaiter taskAwaiter1;
TaskAwaiter<int> taskAwaiter2;
// The following block attempts to get the return value of the
// "await Task.Delay(contemplationTime)" invocation.
if (locState != 0)
{
if (locState != 1)
{
taskAwaiter1 = Task.Delay(this.comtemplationTime).GetAwaiter();
if (!taskAwaiter1.IsCompleted)
{
this.state = 0;
this.awaiter1 = taskAwaiter1;
ExampleStateMachine stateMachine = this;
this.builder.AwaitUnsafeOnCompleted(ref taskAwaiter1, ref stateMachine);
return;
}
}
else
{
taskAwaiter2 = this.awaiter2;
this.awaiter2 = default(TaskAwaiter<int>);
this.state = -1;
goto label_10;
}
}
else
{
taskAwaiter1 = this.awaiter1;
this.awaiter1 = default(TaskAwaiter);
this.state = -1;
}
// At this point the result should have been computed
// by builder.AwaitUnsafeOnCompleted().
taskAwaiter1.GetResult();
if (this.comtemplationTime < 1000)
{
taskAwaiter2 = this.@this.GetAnswerAsync(1000).GetAwaiter();
if (!taskAwaiter2.IsCompleted)
{
this.state = 1;
this.awaiter2 = taskAwaiter2;
ExampleStateMachine stateMachine = this;
this.builder.AwaitUnsafeOnCompleted(ref taskAwaiter2, ref stateMachine);
return;
}
}
else
{
returnValue = 42;
goto label_11;
}
label_10:
returnValue = result1 = taskAwaiter2.GetResult();
taskAwaiter2 = default(TaskAwaiter<int>);
label_11:
returnValue = 42;
}
catch (Exception ex)
{
this.state = -2;
this.builder.SetException(ex);
return;
}
this.state = -2;
this.builder.SetResult(returnValue);
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
// This method is never used, but if it gets called, we should relay it to the builder.
builder.SetStateMachine(stateMachine);
}
}
#endregion
// Dig deeper:
// - How the compiler does it: http://source.roslyn.io/#Microsoft.CodeAnalysis.CSharp/Lowering/AsyncRewriter/AsyncMethodToStateMachineRewriter.cs
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment