Skip to content

Commit 4094e33

Browse files
committed
Add thread-safety and wait-blocking to BenchmarkSynchronizationContext.
1 parent 6f71ff4 commit 4094e33

2 files changed

Lines changed: 143 additions & 30 deletions

File tree

src/BenchmarkDotNet/Engines/BenchmarkSynchronizationContext.cs

Lines changed: 137 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,40 @@ namespace BenchmarkDotNet.Engines;
1010
// Used to ensure async continuations are posted back to the same thread that the benchmark process was started on.
1111
[UsedImplicitly]
1212
[EditorBrowsable(EditorBrowsableState.Never)]
13-
public sealed class BenchmarkSynchronizationContext : SynchronizationContext, IDisposable
13+
public readonly ref struct BenchmarkSynchronizationContext : IDisposable
1414
{
15-
private readonly SynchronizationContext previousContext;
15+
private readonly BenchmarkDotNetSynchronizationContext context;
16+
17+
private BenchmarkSynchronizationContext(BenchmarkDotNetSynchronizationContext context)
18+
{
19+
this.context = context;
20+
}
21+
22+
public static BenchmarkSynchronizationContext CreateAndSetCurrent()
23+
{
24+
var context = new BenchmarkDotNetSynchronizationContext(SynchronizationContext.Current);
25+
SynchronizationContext.SetSynchronizationContext(context);
26+
return new(context);
27+
}
28+
29+
public void Dispose()
30+
=> context.Dispose();
31+
32+
public void ExecuteUntilComplete(ValueTask valueTask)
33+
=> context.ExecuteUntilComplete(valueTask);
34+
35+
public T ExecuteUntilComplete<T>(ValueTask<T> valueTask)
36+
=> context.ExecuteUntilComplete(valueTask);
37+
}
38+
39+
internal sealed class BenchmarkDotNetSynchronizationContext : SynchronizationContext
40+
{
41+
private readonly SynchronizationContext? previousContext;
1642
private readonly Queue<(SendOrPostCallback d, object? state)> queue = new();
43+
private bool isDisposed;
44+
volatile private bool isCompleted;
1745

18-
private BenchmarkSynchronizationContext(SynchronizationContext previousContext)
46+
internal BenchmarkDotNetSynchronizationContext(SynchronizationContext? previousContext)
1947
{
2048
this.previousContext = previousContext;
2149
}
@@ -24,53 +52,133 @@ public override SynchronizationContext CreateCopy()
2452
=> this;
2553

2654
public override void Post(SendOrPostCallback d, object? state)
27-
=> queue.Enqueue((d ?? throw new ArgumentNullException(nameof(d)), state));
55+
{
56+
if (d is null) throw new ArgumentNullException(nameof(d));
2857

29-
public static BenchmarkSynchronizationContext CreateAndSetCurrent()
58+
lock (queue)
59+
{
60+
ThrowIfDisposed();
61+
62+
queue.Enqueue((d, state));
63+
Monitor.Pulse(queue);
64+
}
65+
}
66+
67+
private void ThrowIfDisposed()
3068
{
31-
var context = new BenchmarkSynchronizationContext(Current);
32-
SetSynchronizationContext(context);
33-
return context;
69+
if (isDisposed) throw new ObjectDisposedException(nameof(BenchmarkDotNetSynchronizationContext));
3470
}
3571

36-
public void Dispose()
37-
=> SetSynchronizationContext(previousContext);
72+
internal void Dispose()
73+
{
74+
lock (queue)
75+
{
76+
ThrowIfDisposed();
77+
isDisposed = true;
3878

39-
public void ExecuteUntilComplete(ValueTask valueTask)
79+
// Flush any remaining posted callbacks.
80+
while (TryDequeue(out var callbackAndState))
81+
{
82+
callbackAndState.d(callbackAndState.state);
83+
}
84+
}
85+
SetSynchronizationContext(previousContext);
86+
}
87+
88+
internal void ExecuteUntilComplete(ValueTask valueTask)
4089
{
41-
var spinner = new SpinWait();
42-
while (!valueTask.IsCompleted)
90+
ThrowIfDisposed();
91+
92+
var awaiter = valueTask.GetAwaiter();
93+
if (valueTask.IsCompleted)
4394
{
44-
DoSpin(ref spinner);
95+
awaiter.GetResult();
96+
return;
4597
}
46-
valueTask.GetAwaiter().GetResult();
98+
99+
isCompleted = false;
100+
awaiter.UnsafeOnCompleted(OnCompleted);
101+
ExecuteUntilComplete();
102+
awaiter.GetResult();
47103
}
48104

49-
public T ExecuteUntilComplete<T>(ValueTask<T> valueTask)
105+
internal T ExecuteUntilComplete<T>(ValueTask<T> valueTask)
50106
{
51-
var spinner = new SpinWait();
52-
while (!valueTask.IsCompleted)
107+
ThrowIfDisposed();
108+
109+
var awaiter = valueTask.GetAwaiter();
110+
if (valueTask.IsCompleted)
53111
{
54-
DoSpin(ref spinner);
112+
return awaiter.GetResult();
55113
}
56-
return valueTask.GetAwaiter().GetResult();
114+
115+
isCompleted = false;
116+
awaiter.UnsafeOnCompleted(OnCompleted);
117+
ExecuteUntilComplete();
118+
return awaiter.GetResult();
57119
}
58120

59-
private void DoSpin(ref SpinWait spinner)
121+
private void OnCompleted()
60122
{
61-
if (queue.Count <= 0)
123+
isCompleted = true;
124+
lock (queue)
62125
{
126+
Monitor.Pulse(queue);
127+
}
128+
}
129+
130+
private void ExecuteUntilComplete()
131+
{
132+
var spinner = new SpinWait();
133+
while (true)
134+
{
135+
if (TryDequeue(out var callbackAndState))
136+
{
137+
do
138+
{
139+
callbackAndState.d(callbackAndState.state);
140+
}
141+
while (TryDequeue(out callbackAndState));
142+
// Reset spinner after any posted callback is executed.
143+
spinner = new();
144+
}
145+
146+
if (isCompleted)
147+
{
148+
return;
149+
}
150+
151+
if (spinner.NextSpinWillYield)
152+
{
153+
// Yield the thread and wait for completion or for a posted callback.
154+
lock (queue)
155+
{
156+
Monitor.Wait(queue);
157+
}
158+
// Reset the spinner.
159+
spinner = new();
160+
continue;
161+
}
162+
63163
spinner.SpinOnce();
64-
return;
65164
}
165+
}
66166

67-
do
167+
private bool TryDequeue(out (SendOrPostCallback d, object? state) callbackAndState)
168+
{
169+
lock (queue)
68170
{
69-
var (d, state) = queue.Dequeue();
70-
d(state);
171+
#if NETSTANDARD2_0
172+
if (queue.Count > 0)
173+
{
174+
callbackAndState = queue.Dequeue();
175+
return true;
176+
}
177+
callbackAndState = default;
178+
return false;
179+
#else
180+
return queue.TryDequeue(out callbackAndState);
181+
#endif
71182
}
72-
while (queue.Count > 0);
73-
// Reset spinner after any posted callback is executed.
74-
spinner = new();
75183
}
76184
}

src/BenchmarkDotNet/Templates/BenchmarkProgram.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,17 @@ namespace BenchmarkDotNet.Autogenerated
5959
{
6060
host.WriteLine("You have not specified benchmark id (an integer) so the first benchmark will be executed.");
6161
}
62-
using (global::BenchmarkDotNet.Engines.BenchmarkSynchronizationContext benchmarkSynchronizationContext = global::BenchmarkDotNet.Engines.BenchmarkSynchronizationContext.CreateAndSetCurrent())
62+
global::BenchmarkDotNet.Engines.BenchmarkSynchronizationContext benchmarkSynchronizationContext = global::BenchmarkDotNet.Engines.BenchmarkSynchronizationContext.CreateAndSetCurrent();
63+
try
6364
{
6465
global::System.Threading.Tasks.ValueTask runTask;
6566
$BenchmarkRunCall$
6667
benchmarkSynchronizationContext.ExecuteUntilComplete(runTask);
6768
}
69+
finally
70+
{
71+
benchmarkSynchronizationContext.Dispose();
72+
}
6873
return 0;
6974
}
7075
catch (global::System.Exception oom) when (oom is global::System.OutOfMemoryException || oom is global::System.Reflection.TargetInvocationException reflection && reflection.InnerException is global::System.OutOfMemoryException)

0 commit comments

Comments
 (0)