Last active
July 25, 2024 04:11
-
-
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#.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 = [email protected](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