Skip to content

Commit d9732f4

Browse files
benaadamskouvel
authored andcommitted
Fast-path ExecutionContext for ThreadPool items (dotnet#20308)
Fast-path ExecutionContext for ThreadPool items Maintain the ThreadPool threads on the Default contexts between work items. Always restore the Default context on the ThreadPool Dispatch loop after a workitem has run (to clean up any ExecutionContext leakage from changes on flow suppressed workitems, or AsyncLocal change eventhandlers; as well as firing their notifications if they have them) Store the `CurrentThread` as part of the thread static `ThreadPoolWorkQueueThreadLocals` which are already looked up at the start of the Dispatch loop to avoid an additional lookup via `Thread.CurrentThread`. As workitems are started on the Default context and are returned to it `QueueUserWorkItemCallbackDefaultContext` items can just be run their callbacks directly rather than via `ExecutionContext.Run` (as the EC don't need to move to Default and is always moved back to Default). As `QueueUserWorkItemCallbackDefaultContext` now runs items directly; flow suppressed callbacks can use the smaller `QueueUserWorkItemCallbackDefaultContext` rather than `QueueUserWorkItemCallback` with a null context; and handling for flow suppression can be removed from `QueueUserWorkItemCallback`. As `AwaitTaskContinuation`'s `IThreadPoolWorkItem.Execute` doesn't preform additional work after it completes, it can run `m_action` directly for Default context in addition to the flow suppressed context, rather than going via `ExecutionContext.Run`. Given that the items on the ThreadPool are always started on the threadpool and restored to it; we can introduce some faster paths than `ExecutionContext:RunInternal` (328 bytes asm). Introduce `ExecutionContext:RunForThreadPoolUnsafe` (71 bytes asm), for `IThreadPoolWorkItem`s where they need to run on a provided context, but do not need to execute anything after they complete so can rely on the Dispatch loop restore. This includes `QueueUserWorkItemCallback`, `QueueUserWorkItemCallback<TState>` and `AwaitTaskContinuation`. Introduce `ExecutionContext:RunFromThreadPoolDispatchLoop` (225 bytes asm), for items run from the ThreadPool, so don't need to capture current context (Default) to restore later, however need to do need to restore back to Default after execution as they then perform additional work. This includes `Task`/`AsyncStateMachineBox`/`AwaitTaskContinuation`/`Timer`. Change `Task.ExecuteFromThreadPool()` to take the thread `Task.ExecuteFromThreadPool(Thread threadPoolThread)` from the ThreadPool Dispatch loop so it can pass it into the `ExecutionContext:RunFromThreadPoolDispatchLoop` overload and avoid the `Thread.CurrentThread` lookup. Perf test: dotnet#20308 (comment) Resolves: dotnet/corefx#32695
1 parent 0f6e360 commit d9732f4

File tree

8 files changed

+234
-101
lines changed

8 files changed

+234
-101
lines changed

src/System.Private.CoreLib/shared/System/Threading/ExecutionContext.cs

+120
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
===========================================================*/
1313

1414
using System.Diagnostics;
15+
using System.Runtime.CompilerServices;
1516
using System.Runtime.ExceptionServices;
1617
using System.Runtime.Serialization;
1718

@@ -136,6 +137,11 @@ internal static void RunInternal(ExecutionContext executionContext, ContextCallb
136137
Thread currentThread0 = Thread.CurrentThread;
137138
Thread currentThread = currentThread0;
138139
ExecutionContext previousExecutionCtx0 = currentThread0.ExecutionContext;
140+
if (previousExecutionCtx0 != null && previousExecutionCtx0.m_isDefault)
141+
{
142+
// Default is a null ExecutionContext internally
143+
previousExecutionCtx0 = null;
144+
}
139145

140146
// Store current ExecutionContext and SynchronizationContext as "previousXxx".
141147
// This allows us to restore them and undo any Context changes made in callback.Invoke
@@ -215,6 +221,11 @@ internal static void RunInternal<TState>(ExecutionContext executionContext, Cont
215221
Thread currentThread0 = Thread.CurrentThread;
216222
Thread currentThread = currentThread0;
217223
ExecutionContext previousExecutionCtx0 = currentThread0.ExecutionContext;
224+
if (previousExecutionCtx0 != null && previousExecutionCtx0.m_isDefault)
225+
{
226+
// Default is a null ExecutionContext internally
227+
previousExecutionCtx0 = null;
228+
}
218229

219230
// Store current ExecutionContext and SynchronizationContext as "previousXxx".
220231
// This allows us to restore them and undo any Context changes made in callback.Invoke
@@ -282,6 +293,115 @@ internal static void RunInternal<TState>(ExecutionContext executionContext, Cont
282293
edi?.Throw();
283294
}
284295

296+
internal static void RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, object state)
297+
{
298+
Debug.Assert(threadPoolThread == Thread.CurrentThread);
299+
CheckThreadPoolAndContextsAreDefault();
300+
// ThreadPool starts on Default Context so we don't need to save the "previous" state as we know it is Default (null)
301+
302+
if (executionContext != null && executionContext.m_isDefault)
303+
{
304+
// Default is a null ExecutionContext internally
305+
executionContext = null;
306+
}
307+
else if (executionContext != null)
308+
{
309+
// Non-Default context to restore
310+
threadPoolThread.ExecutionContext = executionContext;
311+
if (executionContext.HasChangeNotifications)
312+
{
313+
// There are change notifications; trigger any affected
314+
OnValuesChanged(previousExecutionCtx: null, executionContext);
315+
}
316+
}
317+
318+
ExceptionDispatchInfo edi = null;
319+
try
320+
{
321+
callback.Invoke(state);
322+
}
323+
catch (Exception ex)
324+
{
325+
// Note: we have a "catch" rather than a "finally" because we want
326+
// to stop the first pass of EH here. That way we can restore the previous
327+
// context before any of our callers' EH filters run.
328+
edi = ExceptionDispatchInfo.Capture(ex);
329+
}
330+
331+
// Enregister threadPoolThread as it crossed EH, and use enregistered variable
332+
Thread currentThread = threadPoolThread;
333+
334+
ExecutionContext currentExecutionCtx = currentThread.ExecutionContext;
335+
336+
// Restore changed SynchronizationContext back to Default
337+
currentThread.SynchronizationContext = null;
338+
if (currentExecutionCtx != null)
339+
{
340+
// The EC always needs to be reset for this overload, as it will flow back to the caller if it performs
341+
// extra work prior to returning to the Dispatch loop. For example for Task-likes it will flow out of await points
342+
343+
// Restore to Default before Notifications, as the change can be observed in the handler.
344+
currentThread.ExecutionContext = null;
345+
if (currentExecutionCtx.HasChangeNotifications)
346+
{
347+
// There are change notifications; trigger any affected
348+
OnValuesChanged(currentExecutionCtx, nextExecutionCtx: null);
349+
}
350+
}
351+
352+
// If exception was thrown by callback, rethrow it now original contexts are restored
353+
edi?.Throw();
354+
}
355+
356+
internal static void RunForThreadPoolUnsafe<TState>(ExecutionContext executionContext, Action<TState> callback, in TState state)
357+
{
358+
// We aren't running in try/catch as if an exception is directly thrown on the ThreadPool either process
359+
// will crash or its a ThreadAbortException.
360+
361+
CheckThreadPoolAndContextsAreDefault();
362+
Debug.Assert(executionContext != null && !executionContext.m_isDefault, "ExecutionContext argument is Default.");
363+
364+
Thread currentThread = Thread.CurrentThread;
365+
// Restore Non-Default context
366+
currentThread.ExecutionContext = executionContext;
367+
if (executionContext.HasChangeNotifications)
368+
{
369+
OnValuesChanged(previousExecutionCtx: null, executionContext);
370+
}
371+
372+
callback.Invoke(state);
373+
374+
// ThreadPoolWorkQueue.Dispatch will handle notifications and reset EC and SyncCtx back to default
375+
}
376+
377+
// Inline as only called in one place and always called
378+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
379+
internal static void ResetThreadPoolThread(Thread currentThread)
380+
{
381+
ExecutionContext currentExecutionCtx = currentThread.ExecutionContext;
382+
383+
// Reset to defaults
384+
currentThread.SynchronizationContext = null;
385+
currentThread.ExecutionContext = null;
386+
387+
if (currentExecutionCtx != null && currentExecutionCtx.HasChangeNotifications)
388+
{
389+
OnValuesChanged(currentExecutionCtx, nextExecutionCtx: null);
390+
391+
// Reset to defaults again without change notifications in case the Change handler changed the contexts
392+
currentThread.SynchronizationContext = null;
393+
currentThread.ExecutionContext = null;
394+
}
395+
}
396+
397+
[System.Diagnostics.Conditional("DEBUG")]
398+
internal static void CheckThreadPoolAndContextsAreDefault()
399+
{
400+
Debug.Assert(Thread.CurrentThread.IsThreadPoolThread);
401+
Debug.Assert(Thread.CurrentThread.ExecutionContext == null, "ThreadPool thread not on Default ExecutionContext.");
402+
Debug.Assert(Thread.CurrentThread.SynchronizationContext == null, "ThreadPool thread not on Default SynchronizationContext.");
403+
}
404+
285405
internal static void OnValuesChanged(ExecutionContext previousExecutionCtx, ExecutionContext nextExecutionCtx)
286406
{
287407
Debug.Assert(previousExecutionCtx != nextExecutionCtx);

src/System.Private.CoreLib/shared/System/Threading/SemaphoreSlim.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ private sealed class TaskNode : Task<bool>
8080
internal TaskNode Prev, Next;
8181
internal TaskNode() : base() { }
8282

83-
internal override void ExecuteFromThreadPool()
83+
internal override void ExecuteFromThreadPool(Thread threadPoolThread)
8484
{
8585
bool setSuccessfully = TrySetResult(true);
8686
Debug.Assert(setSuccessfully, "Should have been able to complete task");

src/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncMethodBuilder.cs

+12-3
Original file line numberDiff line numberDiff line change
@@ -540,10 +540,12 @@ private class AsyncStateMachineBox<TStateMachine> : // SOS DumpAsync command dep
540540
/// <summary>A delegate to the <see cref="MoveNext"/> method.</summary>
541541
public Action MoveNextAction => _moveNextAction ?? (_moveNextAction = new Action(MoveNext));
542542

543-
internal sealed override void ExecuteFromThreadPool() => MoveNext();
543+
internal sealed override void ExecuteFromThreadPool(Thread threadPoolThread) => MoveNext(threadPoolThread);
544544

545545
/// <summary>Calls MoveNext on <see cref="StateMachine"/></summary>
546-
public void MoveNext()
546+
public void MoveNext() => MoveNext(threadPoolThread: null);
547+
548+
private void MoveNext(Thread threadPoolThread)
547549
{
548550
Debug.Assert(!IsCompleted);
549551

@@ -560,7 +562,14 @@ public void MoveNext()
560562
}
561563
else
562564
{
563-
ExecutionContext.RunInternal(context, s_callback, this);
565+
if (threadPoolThread is null)
566+
{
567+
ExecutionContext.RunInternal(context, s_callback, this);
568+
}
569+
else
570+
{
571+
ExecutionContext.RunFromThreadPoolDispatchLoop(threadPoolThread, context, s_callback, this);
572+
}
564573
}
565574

566575
if (IsCompleted)

src/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs

+12-5
Original file line numberDiff line numberDiff line change
@@ -2368,16 +2368,16 @@ internal bool ExecuteEntry()
23682368
/// can override to customize their behavior, which is usually done by promises
23692369
/// that want to reuse the same object as a queued work item.
23702370
/// </summary>
2371-
internal virtual void ExecuteFromThreadPool() => ExecuteEntryUnsafe();
2371+
internal virtual void ExecuteFromThreadPool(Thread threadPoolThread) => ExecuteEntryUnsafe(threadPoolThread);
23722372

2373-
internal void ExecuteEntryUnsafe() // used instead of ExecuteEntry() when we don't have to worry about double-execution prevent
2373+
internal void ExecuteEntryUnsafe(Thread threadPoolThread) // used instead of ExecuteEntry() when we don't have to worry about double-execution prevent
23742374
{
23752375
// Remember that we started running the task delegate.
23762376
m_stateFlags |= TASK_STATE_DELEGATE_INVOKED;
23772377

23782378
if (!IsCancellationRequested & !IsCanceled)
23792379
{
2380-
ExecuteWithThreadLocal(ref t_currentTask);
2380+
ExecuteWithThreadLocal(ref t_currentTask, threadPoolThread);
23812381
}
23822382
else
23832383
{
@@ -2398,7 +2398,7 @@ internal void ExecuteEntryCancellationRequestedOrCanceled()
23982398
}
23992399

24002400
// A trick so we can refer to the TLS slot with a byref.
2401-
private void ExecuteWithThreadLocal(ref Task currentTaskSlot)
2401+
private void ExecuteWithThreadLocal(ref Task currentTaskSlot, Thread threadPoolThread = null)
24022402
{
24032403
// Remember the current task so we can restore it after running, and then
24042404
Task previousTask = currentTaskSlot;
@@ -2439,7 +2439,14 @@ private void ExecuteWithThreadLocal(ref Task currentTaskSlot)
24392439
else
24402440
{
24412441
// Invoke it under the captured ExecutionContext
2442-
ExecutionContext.RunInternal(ec, s_ecCallback, this);
2442+
if (threadPoolThread is null)
2443+
{
2444+
ExecutionContext.RunInternal(ec, s_ecCallback, this);
2445+
}
2446+
else
2447+
{
2448+
ExecutionContext.RunFromThreadPoolDispatchLoop(threadPoolThread, ec, s_ecCallback, this);
2449+
}
24432450
}
24442451
}
24452452
catch (Exception exn)

src/System.Private.CoreLib/src/System/Threading/Tasks/TaskContinuation.cs

+10-14
Original file line numberDiff line numberDiff line change
@@ -646,16 +646,20 @@ void IThreadPoolWorkItem.Execute()
646646
// We're not inside of a task, so t_currentTask doesn't need to be specially maintained.
647647
// We're on a thread pool thread with no higher-level callers, so exceptions can just propagate.
648648

649-
// If there's no execution context, just invoke the delegate.
650-
if (context == null)
649+
ExecutionContext.CheckThreadPoolAndContextsAreDefault();
650+
// If there's no execution context or Default, just invoke the delegate as ThreadPool is on Default context.
651+
// We don't have to use ExecutionContext.Run for the Default context here as there is no extra processing after the delegate
652+
if (context == null || context.IsDefault)
651653
{
652654
m_action();
653655
}
654656
// If there is an execution context, get the cached delegate and run the action under the context.
655657
else
656658
{
657-
ExecutionContext.RunInternal(context, GetInvokeActionCallback(), m_action);
659+
ExecutionContext.RunForThreadPoolUnsafe(context, s_invokeAction, m_action);
658660
}
661+
662+
// ThreadPoolWorkQueue.Dispatch handles notifications and reset context back to default
659663
}
660664
finally
661665
{
@@ -667,19 +671,11 @@ void IThreadPoolWorkItem.Execute()
667671
}
668672

669673
/// <summary>Cached delegate that invokes an Action passed as an object parameter.</summary>
670-
private static ContextCallback s_invokeActionCallback;
671-
672-
/// <summary>Runs an action provided as an object parameter.</summary>
673-
/// <param name="state">The Action to invoke.</param>
674-
private static void InvokeAction(object state) { ((Action)state)(); }
674+
private readonly static ContextCallback s_invokeContextCallback = (state) => ((Action)state)();
675+
private readonly static Action<Action> s_invokeAction = (action) => action();
675676

676677
[MethodImpl(MethodImplOptions.AggressiveInlining)]
677-
protected static ContextCallback GetInvokeActionCallback()
678-
{
679-
ContextCallback callback = s_invokeActionCallback;
680-
if (callback == null) { s_invokeActionCallback = callback = InvokeAction; } // lazily initialize SecurityCritical delegate
681-
return callback;
682-
}
678+
protected static ContextCallback GetInvokeActionCallback() => s_invokeContextCallback;
683679

684680
/// <summary>Runs the callback synchronously with the provided state.</summary>
685681
/// <param name="callback">The callback to run.</param>

src/System.Private.CoreLib/src/System/Threading/Tasks/ThreadPoolTaskScheduler.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ internal ThreadPoolTaskScheduler()
3333
}
3434

3535
// static delegate for threads allocated to handle LongRunning tasks.
36-
private static readonly ParameterizedThreadStart s_longRunningThreadWork = s => ((Task)s).ExecuteEntryUnsafe();
36+
private static readonly ParameterizedThreadStart s_longRunningThreadWork = s => ((Task)s).ExecuteEntryUnsafe(threadPoolThread: null);
3737

3838
/// <summary>
3939
/// Schedules a task to the ThreadPool.
@@ -73,7 +73,7 @@ protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQu
7373

7474
try
7575
{
76-
task.ExecuteEntryUnsafe(); // handles switching Task.Current etc.
76+
task.ExecuteEntryUnsafe(threadPoolThread: null); // handles switching Task.Current etc.
7777
}
7878
finally
7979
{

0 commit comments

Comments
 (0)