Skip to content

Commit 0dc5813

Browse files
committed
Add test case to make sure ValueTasks work properly with a race condition between IsCompleted and OnCompleted.
Changed AwaitHelper to use `ManualResetEventSlim` instead of `Monitor.Wait`.
1 parent b3d8e81 commit 0dc5813

File tree

2 files changed

+99
-24
lines changed

2 files changed

+99
-24
lines changed

src/BenchmarkDotNet/Helpers/AwaitHelper.cs

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Linq;
33
using System.Reflection;
44
using System.Runtime.CompilerServices;
5+
using System.Threading;
56
using System.Threading.Tasks;
67

78
namespace BenchmarkDotNet.Helpers
@@ -10,54 +11,55 @@ public static class AwaitHelper
1011
{
1112
private class ValueTaskWaiter
1213
{
13-
// We use thread static field so that multiple threads can use individual lock object and callback.
14+
// We use thread static field so that each thread uses its own individual callback and reset event.
1415
[ThreadStatic]
1516
private static ValueTaskWaiter ts_current;
1617
internal static ValueTaskWaiter Current => ts_current ??= new ValueTaskWaiter();
1718

19+
// We cache the callback to prevent allocations for memory diagnoser.
1820
private readonly Action awaiterCallback;
19-
private bool awaiterCompleted;
21+
private readonly ManualResetEventSlim resetEvent;
2022

2123
private ValueTaskWaiter()
2224
{
23-
awaiterCallback = AwaiterCallback;
24-
}
25-
26-
private void AwaiterCallback()
27-
{
28-
lock (this)
29-
{
30-
awaiterCompleted = true;
31-
System.Threading.Monitor.Pulse(this);
32-
}
25+
resetEvent = new ();
26+
awaiterCallback = resetEvent.Set;
3327
}
3428

3529
// Hook up a callback instead of converting to Task to prevent extra allocations on each benchmark run.
3630
internal void Wait(ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter)
3731
{
38-
lock (this)
32+
resetEvent.Reset();
33+
awaiter.UnsafeOnCompleted(awaiterCallback);
34+
35+
// The fastest way to wait for completion is to spin a bit before waiting on the event. This is the same logic that Task.GetAwaiter().GetResult() uses.
36+
var spinner = new SpinWait();
37+
while (!resetEvent.IsSet)
3938
{
40-
awaiterCompleted = false;
41-
awaiter.UnsafeOnCompleted(awaiterCallback);
42-
// Check if the callback executed synchronously before blocking.
43-
if (!awaiterCompleted)
39+
if (spinner.NextSpinWillYield)
4440
{
45-
System.Threading.Monitor.Wait(this);
41+
resetEvent.Wait();
42+
return;
4643
}
44+
spinner.SpinOnce();
4745
}
4846
}
4947

5048
internal void Wait<T>(ConfiguredValueTaskAwaitable<T>.ConfiguredValueTaskAwaiter awaiter)
5149
{
52-
lock (this)
50+
resetEvent.Reset();
51+
awaiter.UnsafeOnCompleted(awaiterCallback);
52+
53+
// The fastest way to wait for completion is to spin a bit before waiting on the event. This is the same logic that Task.GetAwaiter().GetResult() uses.
54+
var spinner = new SpinWait();
55+
while (!resetEvent.IsSet)
5356
{
54-
awaiterCompleted = false;
55-
awaiter.UnsafeOnCompleted(awaiterCallback);
56-
// Check if the callback executed synchronously before blocking.
57-
if (!awaiterCompleted)
57+
if (spinner.NextSpinWillYield)
5858
{
59-
System.Threading.Monitor.Wait(this);
59+
resetEvent.Wait();
60+
return;
6061
}
62+
spinner.SpinOnce();
6163
}
6264
}
6365
}

tests/BenchmarkDotNet.IntegrationTests/AsyncBenchmarksTests.cs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,23 @@ internal class ValueTaskSource<T> : IValueTaskSource<T>, IValueTaskSource
2222
public void SetResult(T result) => _core.SetResult(result);
2323
}
2424

25+
// This is used to test the case of ValueTaskAwaiter.IsCompleted returns false, then OnCompleted invokes the callback immediately because it happened to complete between the 2 calls.
26+
internal class ValueTaskSourceCallbackOnly<T> : IValueTaskSource<T>, IValueTaskSource
27+
{
28+
private ManualResetValueTaskSourceCore<T> _core;
29+
30+
T IValueTaskSource<T>.GetResult(short token) => _core.GetResult(token);
31+
void IValueTaskSource.GetResult(short token) => _core.GetResult(token);
32+
// Always return pending state so OnCompleted will be called.
33+
ValueTaskSourceStatus IValueTaskSource<T>.GetStatus(short token) => ValueTaskSourceStatus.Pending;
34+
ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => ValueTaskSourceStatus.Pending;
35+
void IValueTaskSource<T>.OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) => _core.OnCompleted(continuation, state, token, flags);
36+
void IValueTaskSource.OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) => _core.OnCompleted(continuation, state, token, flags);
37+
public void Reset() => _core.Reset();
38+
public short Token => _core.Version;
39+
public void SetResult(T result) => _core.SetResult(result);
40+
}
41+
2542
public class AsyncBenchmarksTests : BenchmarkTestExecutor
2643
{
2744
public AsyncBenchmarksTests(ITestOutputHelper output) : base(output) { }
@@ -41,6 +58,9 @@ public void TaskReturningMethodsAreAwaited()
4158
}
4259
}
4360

61+
[Fact]
62+
public void TaskReturningMethodsAreAwaited_AlreadyComplete() => CanExecute<TaskDelayMethods>();
63+
4464
public class TaskDelayMethods
4565
{
4666
private readonly ValueTaskSource<int> valueTaskSource = new ();
@@ -89,5 +109,58 @@ public ValueTask<int> ReturningGenericValueTaskBackByIValueTaskSource()
89109
return new ValueTask<int>(valueTaskSource, valueTaskSource.Token);
90110
}
91111
}
112+
113+
public class TaskImmediateMethods
114+
{
115+
private readonly ValueTaskSource<int> valueTaskSource = new ();
116+
private readonly ValueTaskSourceCallbackOnly<int> valueTaskSourceCallbackOnly = new ();
117+
118+
[Benchmark]
119+
public Task ReturningTask() => Task.CompletedTask;
120+
121+
[Benchmark]
122+
public ValueTask ReturningValueTask() => new ValueTask();
123+
124+
[Benchmark]
125+
public ValueTask ReturningValueTaskBackByIValueTaskSource()
126+
{
127+
valueTaskSource.Reset();
128+
valueTaskSource.SetResult(default);
129+
return new ValueTask(valueTaskSource, valueTaskSource.Token);
130+
}
131+
132+
[Benchmark]
133+
public ValueTask ReturningValueTaskBackByIValueTaskSource_ImmediateCallback()
134+
{
135+
valueTaskSourceCallbackOnly.Reset();
136+
valueTaskSourceCallbackOnly.SetResult(default);
137+
return new ValueTask(valueTaskSourceCallbackOnly, valueTaskSourceCallbackOnly.Token);
138+
}
139+
140+
[Benchmark]
141+
public async Task Awaiting() => await Task.CompletedTask;
142+
143+
[Benchmark]
144+
public Task<int> ReturningGenericTask() => ReturningTask().ContinueWith(_ => default(int));
145+
146+
[Benchmark]
147+
public ValueTask<int> ReturningGenericValueTask() => new ValueTask<int>(ReturningGenericTask());
148+
149+
[Benchmark]
150+
public ValueTask<int> ReturningGenericValueTaskBackByIValueTaskSource()
151+
{
152+
valueTaskSource.Reset();
153+
valueTaskSource.SetResult(default);
154+
return new ValueTask<int>(valueTaskSource, valueTaskSource.Token);
155+
}
156+
157+
[Benchmark]
158+
public ValueTask<int> ReturningGenericValueTaskBackByIValueTaskSource_ImmediateCallback()
159+
{
160+
valueTaskSourceCallbackOnly.Reset();
161+
valueTaskSourceCallbackOnly.SetResult(default);
162+
return new ValueTask<int>(valueTaskSourceCallbackOnly, valueTaskSourceCallbackOnly.Token);
163+
}
164+
}
92165
}
93166
}

0 commit comments

Comments
 (0)