Skip to content

Commit 4b3d20a

Browse files
committed
Adjust AwaitHelper to allow multiple threads to use it concurrently.
1 parent 5720494 commit 4b3d20a

1 file changed

Lines changed: 67 additions & 49 deletions

File tree

src/BenchmarkDotNet/Helpers/AwaitHelper.cs

Lines changed: 67 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -8,77 +8,95 @@ namespace BenchmarkDotNet.Helpers
88
{
99
public class AwaitHelper
1010
{
11-
private readonly object awaiterLock = new object();
12-
private readonly Action awaiterCallback;
13-
private bool awaiterCompleted;
14-
15-
public AwaitHelper()
11+
private class ValueTaskWaiter
1612
{
17-
awaiterCallback = AwaiterCallback;
18-
}
13+
private readonly Action awaiterCallback;
14+
private bool awaiterCompleted;
1915

20-
private void AwaiterCallback()
21-
{
22-
lock (awaiterLock)
16+
internal ValueTaskWaiter()
2317
{
24-
awaiterCompleted = true;
25-
System.Threading.Monitor.Pulse(awaiterLock);
18+
awaiterCallback = AwaiterCallback;
2619
}
27-
}
2820

29-
// we use GetAwaiter().GetResult() because it's fastest way to obtain the result in blocking way,
30-
// and will eventually throw actual exception, not aggregated one
31-
public void GetResult(Task task)
32-
{
33-
task.GetAwaiter().GetResult();
34-
}
21+
private void AwaiterCallback()
22+
{
23+
lock (this)
24+
{
25+
awaiterCompleted = true;
26+
System.Threading.Monitor.Pulse(this);
27+
}
28+
}
3529

36-
public T GetResult<T>(Task<T> task)
37-
{
38-
return task.GetAwaiter().GetResult();
39-
}
30+
// Hook up a callback instead of converting to Task to prevent extra allocations on each benchmark run.
31+
internal void GetResult(ValueTask task)
32+
{
33+
// Don't continue on the captured context, as that may result in a deadlock if the user runs this in-process.
34+
var awaiter = task.ConfigureAwait(false).GetAwaiter();
35+
if (!awaiter.IsCompleted)
36+
{
37+
lock (this)
38+
{
39+
awaiterCompleted = false;
40+
awaiter.UnsafeOnCompleted(awaiterCallback);
41+
// Check if the callback executed synchronously before blocking.
42+
if (!awaiterCompleted)
43+
{
44+
System.Threading.Monitor.Wait(this);
45+
}
46+
}
47+
}
48+
awaiter.GetResult();
49+
}
4050

41-
// It is illegal to call GetResult from an uncomplete ValueTask, so we must hook up a callback.
42-
public void GetResult(ValueTask task)
43-
{
44-
// Don't continue on the captured context, as that may result in a deadlock if the user runs this in-process.
45-
var awaiter = task.ConfigureAwait(false).GetAwaiter();
46-
if (!awaiter.IsCompleted)
51+
internal T GetResult<T>(ValueTask<T> task)
4752
{
48-
lock (awaiterLock)
53+
// Don't continue on the captured context, as that may result in a deadlock if the user runs this in-process.
54+
var awaiter = task.ConfigureAwait(false).GetAwaiter();
55+
if (!awaiter.IsCompleted)
4956
{
50-
awaiterCompleted = false;
51-
awaiter.UnsafeOnCompleted(awaiterCallback);
52-
// Check if the callback executed synchronously before blocking.
53-
if (!awaiterCompleted)
57+
lock (this)
5458
{
55-
System.Threading.Monitor.Wait(awaiterLock);
59+
awaiterCompleted = false;
60+
awaiter.UnsafeOnCompleted(awaiterCallback);
61+
// Check if the callback executed synchronously before blocking.
62+
if (!awaiterCompleted)
63+
{
64+
System.Threading.Monitor.Wait(this);
65+
}
5666
}
5767
}
68+
return awaiter.GetResult();
5869
}
59-
awaiter.GetResult();
6070
}
6171

62-
public T GetResult<T>(ValueTask<T> task)
72+
// We use thread static field so that multiple threads can use individual lock object and callback.
73+
[ThreadStatic]
74+
private static ValueTaskWaiter ts_valueTaskWaiter;
75+
76+
private ValueTaskWaiter CurrentValueTaskWaiter
6377
{
64-
// Don't continue on the captured context, as that may result in a deadlock if the user runs this in-process.
65-
var awaiter = task.ConfigureAwait(false).GetAwaiter();
66-
if (!awaiter.IsCompleted)
78+
get
6779
{
68-
lock (awaiterLock)
80+
if (ts_valueTaskWaiter == null)
6981
{
70-
awaiterCompleted = false;
71-
awaiter.UnsafeOnCompleted(awaiterCallback);
72-
// Check if the callback executed synchronously before blocking.
73-
if (!awaiterCompleted)
74-
{
75-
System.Threading.Monitor.Wait(awaiterLock);
76-
}
82+
ts_valueTaskWaiter = new ValueTaskWaiter();
7783
}
84+
return ts_valueTaskWaiter;
7885
}
79-
return awaiter.GetResult();
8086
}
8187

88+
// we use GetAwaiter().GetResult() because it's fastest way to obtain the result in blocking way,
89+
// and will eventually throw actual exception, not aggregated one
90+
public void GetResult(Task task) => task.GetAwaiter().GetResult();
91+
92+
public T GetResult<T>(Task<T> task) => task.GetAwaiter().GetResult();
93+
94+
// ValueTask can be backed by an IValueTaskSource that only supports asynchronous awaits, so we have to hook up a callback instead of calling .GetAwaiter().GetResult() like we do for Task.
95+
// The alternative is to convert it to Task using .AsTask(), but that causes allocations which we must avoid for memory diagnoser.
96+
public void GetResult(ValueTask task) => CurrentValueTaskWaiter.GetResult(task);
97+
98+
public T GetResult<T>(ValueTask<T> task) => CurrentValueTaskWaiter.GetResult(task);
99+
82100
internal static MethodInfo GetGetResultMethod(Type taskType)
83101
{
84102
if (!taskType.IsGenericType)

0 commit comments

Comments
 (0)