Skip to content

Commit 4cb0703

Browse files
authored
Suppress ExecutionContext flow and set BenchmarkSynchronizationContext in InProcess background threads. (#3115)
1 parent 0a8fa7a commit 4cb0703

3 files changed

Lines changed: 66 additions & 32 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
namespace BenchmarkDotNet.Helpers;
2+
3+
internal static class ExecutionContextHelper
4+
{
5+
#if NETCOREAPP
6+
internal static AsyncFlowControl SuppressFlow()
7+
=> ExecutionContext.SuppressFlow();
8+
#else
9+
// .Net Framework throws if ExecutionContext.SuppressFlow() is called when it's already suppressed.
10+
internal static WrappedAsyncFlowControl SuppressFlow()
11+
=> ExecutionContext.IsFlowSuppressed() ? default : new WrappedAsyncFlowControl(ExecutionContext.SuppressFlow());
12+
13+
internal readonly struct WrappedAsyncFlowControl(AsyncFlowControl asyncFlowControl) : IDisposable
14+
{
15+
public void Dispose()
16+
{
17+
if (asyncFlowControl != default)
18+
{
19+
asyncFlowControl.Dispose();
20+
}
21+
}
22+
}
23+
#endif
24+
}

src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitExecutor.cs

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,28 +21,33 @@ public async ValueTask<ExecuteResult> ExecuteAsync(ExecuteParameters executePara
2121
if (executeOnSeparateThread)
2222
{
2323
var taskCompletionSource = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
24-
var runThread = new Thread(async () =>
24+
Thread runThread;
25+
// Ensure the new thread is started with a fresh execution context.
26+
using (ExecutionContextHelper.SuppressFlow())
2527
{
26-
try
28+
runThread = new Thread(() =>
2729
{
28-
taskCompletionSource.SetResult(await ExecuteCore(host, executeParameters).ConfigureAwait(false));
29-
}
30-
catch (Exception ex)
30+
// The engine yields immediately, so we need to set the synchronization context to ensure the benchmarks are executed on this dedicated thread, and not on a ThreadPool thread.
31+
using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent();
32+
try
33+
{
34+
var executeTask = ExecuteCore(host, executeParameters);
35+
taskCompletionSource.SetResult(context.ExecuteUntilComplete(executeTask));
36+
}
37+
catch (Exception ex)
38+
{
39+
taskCompletionSource.SetException(ex);
40+
}
41+
});
42+
if (executeParameters.BenchmarkCase.Descriptor.WorkloadMethod.GetCustomAttributes<STAThreadAttribute>(false).Any()
43+
&& OsDetector.IsWindows())
3144
{
32-
taskCompletionSource.SetException(ex);
45+
runThread.SetApartmentState(ApartmentState.STA);
3346
}
34-
});
35-
36-
if (executeParameters.BenchmarkCase.Descriptor.WorkloadMethod.GetCustomAttributes<STAThreadAttribute>(false).Any()
37-
&& OsDetector.IsWindows())
38-
{
39-
runThread.SetApartmentState(ApartmentState.STA);
47+
runThread.IsBackground = true;
48+
runThread.Start();
4049
}
4150

42-
runThread.IsBackground = true;
43-
44-
runThread.Start();
45-
4651
exitCode = await taskCompletionSource.Task.ConfigureAwait(true);
4752
runThread.Join();
4853
}

src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitExecutor.cs

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,28 +21,33 @@ public async ValueTask<ExecuteResult> ExecuteAsync(ExecuteParameters executePara
2121
if (executeOnSeparateThread)
2222
{
2323
var taskCompletionSource = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
24-
var runThread = new Thread(async () =>
24+
Thread runThread;
25+
// Ensure the new thread is started with a fresh execution context.
26+
using (ExecutionContextHelper.SuppressFlow())
2527
{
26-
try
28+
runThread = new Thread(() =>
2729
{
28-
taskCompletionSource.SetResult(await ExecuteCore(host, executeParameters, benchmarkActionFactory).ConfigureAwait(false));
29-
}
30-
catch (Exception ex)
30+
// The engine yields immediately, so we need to set the synchronization context to ensure the benchmarks are executed on this dedicated thread, and not on a ThreadPool thread.
31+
using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent();
32+
try
33+
{
34+
var executeTask = ExecuteCore(host, executeParameters, benchmarkActionFactory);
35+
taskCompletionSource.SetResult(context.ExecuteUntilComplete(executeTask));
36+
}
37+
catch (Exception ex)
38+
{
39+
taskCompletionSource.SetException(ex);
40+
}
41+
});
42+
if (executeParameters.BenchmarkCase.Descriptor.WorkloadMethod.GetCustomAttributes<STAThreadAttribute>(false).Any()
43+
&& OsDetector.IsWindows())
3144
{
32-
taskCompletionSource.SetException(ex);
45+
runThread.SetApartmentState(ApartmentState.STA);
3346
}
34-
});
35-
36-
if (executeParameters.BenchmarkCase.Descriptor.WorkloadMethod.GetCustomAttributes<STAThreadAttribute>(false).Any()
37-
&& OsDetector.IsWindows())
38-
{
39-
runThread.SetApartmentState(ApartmentState.STA);
47+
runThread.IsBackground = true;
48+
runThread.Start();
4049
}
4150

42-
runThread.IsBackground = true;
43-
44-
runThread.Start();
45-
4651
exitCode = await taskCompletionSource.Task.ConfigureAwait(true);
4752
runThread.Join();
4853
}

0 commit comments

Comments
 (0)