@@ -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