I'm trying to implement my own async method builder for a custom awaitable type. My awaitable type is just a struct containing a ValueTask<T>
.
The problem is my asynchronous method builder only works when it's a class
or compiled in Debug mode, not a struct
and in Release mode.
Here's a minimal, reproducible example. You have to copy this code into a new console project on your local PC and run it in Release mode; .NET Fiddle apparently runs snippets in Debug mode. And of course this requires .Net 5+: https://dotnetfiddle.net/S6F9Hd
This code completes successfully when CustomAwaitableAsyncMethodBuilder<T>
is a class or it is compiled in Debug mode. But it hangs and fails to complete otherwise:
class Program
{
static async Task Main()
{
var expected = Guid.NewGuid().ToString();
async CustomAwaitable<string> GetValueAsync()
{
await Task.Yield();
return expected;
}
var actual = await GetValueAsync();
if (!ReferenceEquals(expected, actual))
throw new Exception();
Console.WriteLine("Done!");
}
}
Here is my custom awaitable type:
[AsyncMethodBuilder(typeof(CustomAwaitableAsyncMethodBuilder<>))]
public readonly struct CustomAwaitable<T>
{
readonly ValueTask<T> _valueTask;
public CustomAwaitable(ValueTask<T> valueTask)
{
_valueTask = valueTask;
}
public ValueTaskAwaiter<T> GetAwaiter() => _valueTask.GetAwaiter();
}
And here is my custom async method builder. Again, all I have to do to make the code run is change this from a struct
to a class
:
public struct CustomAwaitableAsyncMethodBuilder<T>
{
Exception? _exception;
bool _hasResult;
SpinLock _lock;
T? _result;
TaskCompletionSource<T>? _source;
public CustomAwaitable<T> Task
{
get
{
var lockTaken = false;
try
{
_lock.Enter(ref lockTaken);
if (_exception is not null)
return new CustomAwaitable<T>(ValueTask.FromException<T>(_exception));
if (_hasResult)
return new CustomAwaitable<T>(ValueTask.FromResult(_result!));
return new CustomAwaitable<T>(
new ValueTask<T>(
(_source ??= new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously))
.Task
)
);
}
finally
{
if (lockTaken)
_lock.Exit();
}
}
}
public void AwaitOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter,
ref TStateMachine stateMachine)
where TAwaiter : INotifyCompletion
where TStateMachine : IAsyncStateMachine =>
awaiter.OnCompleted(stateMachine.MoveNext);
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter,
ref TStateMachine stateMachine)
where TAwaiter : ICriticalNotifyCompletion
where TStateMachine : IAsyncStateMachine =>
awaiter.UnsafeOnCompleted(stateMachine.MoveNext);
public static CustomAwaitableAsyncMethodBuilder<T> Create() => new()
{
_lock = new SpinLock(Debugger.IsAttached)
};
public void SetException(Exception exception)
{
var lockTaken = false;
try
{
_lock.Enter(ref lockTaken);
if (Volatile.Read(ref _source) is {} source)
{
source.TrySetException(exception);
}
else
{
_exception = exception;
}
}
finally
{
if (lockTaken)
_lock.Exit();
}
}
public void SetResult(T result)
{
var lockTaken = false;
try
{
_lock.Enter(ref lockTaken);
if (Volatile.Read(ref _source) is {} source)
{
source.TrySetResult(result);
}
else
{
_result = result;
_hasResult = true;
}
}
finally
{
if (lockTaken)
_lock.Exit();
}
}
public void SetStateMachine(IAsyncStateMachine stateMachine) {}
public void Start<TStateMachine>(ref TStateMachine stateMachine)
where TStateMachine : IAsyncStateMachine => stateMachine.MoveNext();
}
I've noticed (by debugging this code in Release mode and setting lots of breakpoints) that when it's a struct
in Release mode then this sequence of events happens:
CustomAwaitableAsyncMethodBuilder<T>.Create()
is called exactly onceCustomAwaitableAsyncMethodBuilder<T>.Task
property is accessedCustomAwaitableAsyncMethodBuilder<T>.SetResult(T)
method is invokedTask
property and the SetResult
method both execute concurrentlySpinLock
does not mutually exclude the respective blocks of code (they end up racing)Task
property are not visible to the code in the SetResult
method, nor vice versa#5 and #6 tell me that the Task
property is accessed on one instance of the struct while the SetResult
method is called on a different instance.
Why is that? What am I doing wrong?
I don't see much information out there for how exactly to implement an async method builder. The only things I have found are:
AsyncTaskMethodBuilder<TResult>
I've followed Microsoft's documentation. It even states
The builder type is a
class
orstruct
...and also Microsoft's AsyncTaskMethodBuilder<TResult>
is a struct.
There is one place where their documentation is incorrect (it states that the builder's "AwaitUnsafeOnCompleted()
[method] should call awaiter.OnCompleted(action)
", but AsyncTaskMethodBuilder
doesn't do that). But other than that I assume their documentation is correct.
The only difference I can spot between my implementation and the two other implementations linked above is that mine stores result/exception inside the builder type itself, where they always delegate to a reference type to do that job.
Edit:
ILSpy says this is how my Main
method's state machine is implemented:
// BrokenAsyncMethodBuilder.Program.<Main>d__0
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
[StructLayout(LayoutKind.Auto)]
[CompilerGenerated]
private struct <Main>d__0 : IAsyncStateMachine
{
public int <>1__state;
public AsyncTaskMethodBuilder <>t__builder;
private <>c__DisplayClass0_0 <>8__1;
private ValueTaskAwaiter<string> <>u__1;
private void MoveNext()
{
int num = <>1__state;
try
{
ValueTaskAwaiter<string> awaiter;
if (num != 0)
{
<>8__1 = new <>c__DisplayClass0_0();
<>8__1.expected = Guid.NewGuid().ToString();
awaiter = GetValueAsync().GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (<>1__state = 0);
<>u__1 = awaiter;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
return;
}
}
else
{
awaiter = <>u__1;
<>u__1 = default(ValueTaskAwaiter<string>);
num = (<>1__state = -1);
}
string actual = awaiter.GetResult();
if ((object)<>8__1.expected != actual)
{
throw new Exception();
}
Console.WriteLine("Done!");
}
catch (Exception exception)
{
<>1__state = -2;
<>8__1 = null;
<>t__builder.SetException(exception);
return;
}
<>1__state = -2;
<>8__1 = null;
<>t__builder.SetResult();
async CustomAwaitable<string> GetValueAsync()
{
await Task.Yield();
return ((<>c__DisplayClass0_0)(object)this).expected;
}
}
void IAsyncStateMachine.MoveNext()
{
//ILSpy generated this explicit interface implementation from .override directive in MoveNext
this.MoveNext();
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
<>t__builder.SetStateMachine(stateMachine);
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
//ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
this.SetStateMachine(stateMachine);
}
}
And this is how my GetValueAsync
method's state machine is implemented:
// BrokenAsyncMethodBuilder.Program.<>c__DisplayClass0_0.<<Main>g__GetValueAsync|0>d
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
[StructLayout(LayoutKind.Auto)]
private struct <<Main>g__GetValueAsync|0>d : IAsyncStateMachine
{
public int <>1__state;
public CustomAwaitableAsyncMethodBuilder<string> <>t__builder;
public <>c__DisplayClass0_0 <>4__this;
private YieldAwaitable.YieldAwaiter <>u__1;
private void MoveNext()
{
int num = <>1__state;
<>c__DisplayClass0_0 <>c__DisplayClass0_ = <>4__this;
string expected;
try
{
YieldAwaitable.YieldAwaiter awaiter;
if (num != 0)
{
awaiter = Task.Yield().GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (<>1__state = 0);
<>u__1 = awaiter;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
return;
}
}
else
{
awaiter = <>u__1;
<>u__1 = default(YieldAwaitable.YieldAwaiter);
num = (<>1__state = -1);
}
awaiter.GetResult();
expected = <>c__DisplayClass0_.expected;
}
catch (Exception exception)
{
<>1__state = -2;
<>t__builder.SetException(exception);
return;
}
<>1__state = -2;
<>t__builder.SetResult(expected);
}
void IAsyncStateMachine.MoveNext()
{
//ILSpy generated this explicit interface implementation from .override directive in MoveNext
this.MoveNext();
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
<>t__builder.SetStateMachine(stateMachine);
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
//ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
this.SetStateMachine(stateMachine);
}
}
I'm unable to find this call into CustomAwaitableAsyncMethodBuilder<T>.Create()
, other than it rather unhelpfully says it is used by the Main
method (which decompiles to be very similar to the code at the top of this question). Perhaps I'm not familiar enough with ILSpy, or perhaps it has a bug:
Debug asynchronous code Debugging asynchronous code is a challenge because the tasks are often scheduled in one thread and executed in another. Every thread has its own stacktrace, making it difficult to figure out what happened before the thread started.
When a asynchronous method is executed, the code runs but nothing happens other than a compiler warning.
Async code is a great way to keep your app's UI responsive. You can start an async operation from the UI thread, await it without blocking the UI thread, and naturally resume on the UI thread when it's done. This is a very powerful feature, and most of the time you don't even need to think about it; it “just works”.
Async void methods can wreak havoc if the caller isn't expecting them to be async. When the return type is Task, the caller knows it's dealing with a future operation; when the return type is void, the caller might assume the method is complete by the time it returns.
Found it! If you use ILSpy to disassemble the .dll compiled from the question's code (use the .NET Fiddle link and follow the question's instructions), and then turn ILSpy's language version down to C# 4 (which was the version before async/await was introduced), then you'll see that this is how the GetValueAsync
method is implemented:
// BrokenAsyncMethodBuilder.Program.<>c__DisplayClass0_0
using System.Runtime.CompilerServices;
[AsyncStateMachine(typeof(<<Main>g__GetValueAsync|0>d))]
[return: System.Runtime.CompilerServices.Nullable(new byte[] { 0, 1 })]
internal CustomAwaitable<string> <Main>g__GetValueAsync|0()
{
<<Main>g__GetValueAsync|0>d stateMachine = default(<<Main>g__GetValueAsync|0>d);
stateMachine.<>t__builder = CustomAwaitableAsyncMethodBuilder<string>.Create();
stateMachine.<>4__this = this;
stateMachine.<>1__state = -1;
stateMachine.<>t__builder.Start(ref stateMachine);
return stateMachine.<>t__builder.Task;
}
You can reference the disassembly at the end of the question to learn how the <<Main>g__GetValueAsync|0>d
type (the state machine for the GetValueAsync
method) is implemented. You'll notice that the stateMachine.<>t__builder
field is the CustomAwaitableAsyncMethodBuilder
type (the async method builder in the question).
Now pay attention to what happens:
Start
method is invoked with a reference to the state machine
.MoveNext()
on the state machineMoveNext
calls AwaitUnsafeOnCompleted()
on the async method builderUnsafeOnCompleted()
on the awaiter (the awaiter is a YieldAwaitable.YieldAwaiter
), giving it the state machine's MoveNext
method as a delegateYieldAwaitable.YieldAwaiter.UnsafeOnCompleted
posts the MoveNext
delegate to the current synchronization context to be executed in a little bitTask
property is accessed and returnedNotice also how the next time that the state machine's MoveNext
method is invoked, it will call SetResult
on the async method builder.
So at this point there is a delegate for the state machine's MoveNext
method floating around in the synchronization context. Which means a copy of the state machine has been boxed up and placed on the heap. And the state machine holds a CustomAwaitableAsyncMethodBuilder
, which is itself a struct. So it gets copied too. If CustomAwaitableAsyncMethodBuilder
were a class then it would live on the heap and its reference would be copied instead of its value.
So by the time the GetValueAsync
method returns, there will be a boxed instance of CustomAwaitableAsyncMethodBuilder
on the heap, and another instance of CustomAwaitableAsyncMethodBuilder
on the stack.
I do not know why this does not happen in debug builds. Nor do I yet understand what to do about this.
But this explains why "the Task
property is accessed on one instance of the struct while the SetResult
method is called on a different instance."
Thinking about this a little more, I think this is just another disguise for the fact that C#'s async/await system requires heap allocation and dynamic dispatch by design.
I wrote about this in a little depth not long ago as I compared C#'s async/await with Rust's implementation. In that article I pointed out how C#'s awaitable expressions are composed with dynamic dispatch on heap objects (the compiler packages up a continuation/state machine step as an Action
, and Action
is a reference type). But in Rust, awaitable expressions are composed with static dispatch (which doesn't necessarily require the heap).
This is exactly the same thing, just looking at it from the async method builder's perspective instead of the awaitable expression's perspective. Continuations are packaged up as state machine steps (the MoveNext
method). It doesn't matter if the state machine is itself a value type; it will always get boxed when you treat its MoveNext
method as an Action
delegate. And in this case, boxing the state machine struct
also copied the async method builder struct
.
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