Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit 1bf8191

Browse files
committed
Fix ValueTask behavior as async return type
There's a bug in ValueTask<T>'s builder based on a misunderstanding of what the codegen would be from the compiler. I'd thought the compiler would codegen access to the builder's Task property, but that was based on a very old model; the model actualy in place relies on the Await*OnCompleted methods doing any initialization that's necessary prior to the await. That's an important distinction, because the builder for ValueTask needs defer to the wrapped task builder's Task property once any await has been issued. Which means that rather than just delegating to the wrapped builder's Await*OnCompleted methods, before doing so the ValueTask's builder must remember that these methods have been called, such that a subsequent access will defer to the wrapped Task, even if a result is set in the interim.
1 parent 380ef66 commit 1bf8191

File tree

2 files changed

+39
-3
lines changed

2 files changed

+39
-3
lines changed

src/System.Threading.Tasks.Extensions/src/System/Runtime/CompilerServices/AsyncValueTaskMethodBuilder.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref
8484
where TAwaiter : INotifyCompletion
8585
where TStateMachine : IAsyncStateMachine
8686
{
87+
_useBuilder = true;
8788
_methodBuilder.AwaitOnCompleted(ref awaiter, ref stateMachine);
8889
}
8990

@@ -97,6 +98,7 @@ public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter
9798
where TAwaiter : ICriticalNotifyCompletion
9899
where TStateMachine : IAsyncStateMachine
99100
{
101+
_useBuilder = true;
100102
_methodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
101103
}
102104
}

src/System.Threading.Tasks.Extensions/tests/AsyncValueTaskMethodBuilderTests.cs

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public void SetResult_BeforeAccessTask_ValueTaskContainsValue()
2323
b.SetResult(42);
2424
ValueTask<int> vt = b.Task;
2525
Assert.True(vt.IsCompletedSuccessfully);
26-
Assert.NotSame(vt.AsTask(), vt.AsTask()); // will be different if completed synchronously
26+
Assert.False(WrapsTask(vt));
2727
Assert.Equal(42, vt.Result);
2828
}
2929

@@ -34,7 +34,7 @@ public void SetResult_AfterAccessTask_ValueTaskContainsValue()
3434
ValueTask<int> vt = b.Task;
3535
b.SetResult(42);
3636
Assert.True(vt.IsCompletedSuccessfully);
37-
Assert.Same(vt.AsTask(), vt.AsTask()); // will be safe if completed asynchronously
37+
Assert.True(WrapsTask(vt));
3838
Assert.Equal(42, vt.Result);
3939
}
4040

@@ -111,6 +111,37 @@ public async Task AwaitOnCompleted_InvokesStateMachineMethods(bool awaitUnsafe)
111111
Assert.Equal(dsm, foundSm);
112112
}
113113

114+
[Theory]
115+
[InlineData(1, false)]
116+
[InlineData(2, false)]
117+
[InlineData(1, true)]
118+
[InlineData(2, true)]
119+
public void AwaitOnCompleted_ForcesTaskCreation(int numAwaits, bool awaitUnsafe)
120+
{
121+
AsyncValueTaskMethodBuilder<int> b = ValueTask<int>.CreateAsyncMethodBuilder();
122+
123+
var dsm = new DelegateStateMachine();
124+
TaskAwaiter<int> t = new TaskCompletionSource<int>().Task.GetAwaiter();
125+
126+
Assert.InRange(numAwaits, 1, int.MaxValue);
127+
for (int i = 1; i <= numAwaits; i++)
128+
{
129+
if (awaitUnsafe)
130+
{
131+
b.AwaitUnsafeOnCompleted(ref t, ref dsm);
132+
}
133+
else
134+
{
135+
b.AwaitOnCompleted(ref t, ref dsm);
136+
}
137+
}
138+
139+
b.SetResult(42);
140+
141+
Assert.True(WrapsTask(b.Task));
142+
Assert.Equal(42, b.Task.Result);
143+
}
144+
114145
[Fact]
115146
public void SetStateMachine_InvalidArgument_ThrowsException()
116147
{
@@ -150,9 +181,12 @@ public void Start_ExecutionContextChangesInMoveNextDontFlowOut()
150181
// Make sure we've not caused the Task to be allocated
151182
b.SetResult(42);
152183
ValueTask<int> vt = b.Task;
153-
Assert.NotSame(vt.AsTask(), vt.AsTask());
184+
Assert.False(WrapsTask(vt));
154185
}
155186

187+
/// <summary>Gets whether the ValueTask has a non-null Task.</summary>
188+
private static bool WrapsTask<T>(ValueTask<T> vt) => ReferenceEquals(vt.AsTask(), vt.AsTask());
189+
156190
private struct DelegateStateMachine : IAsyncStateMachine
157191
{
158192
internal Action MoveNextDelegate;

0 commit comments

Comments
 (0)