1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
5 /*============================================================
9 ** Purpose: Capture execution context for a thread
12 ===========================================================*/
14 using System.Diagnostics;
15 using System.Runtime.ExceptionServices;
16 using System.Runtime.Serialization;
18 using Thread = Internal.Runtime.Augments.RuntimeThread;
20 namespace System.Threading
22 public delegate void ContextCallback(Object state);
24 public sealed class ExecutionContext : IDisposable, ISerializable
26 internal static readonly ExecutionContext Default = new ExecutionContext(isDefault: true);
28 private readonly IAsyncLocalValueMap m_localValues;
29 private readonly IAsyncLocal[] m_localChangeNotifications;
30 private readonly bool m_isFlowSuppressed;
31 private readonly bool m_isDefault;
33 private ExecutionContext(bool isDefault)
35 m_isDefault = isDefault;
38 private ExecutionContext(
39 IAsyncLocalValueMap localValues,
40 IAsyncLocal[] localChangeNotifications,
41 bool isFlowSuppressed)
43 m_localValues = localValues;
44 m_localChangeNotifications = localChangeNotifications;
45 m_isFlowSuppressed = isFlowSuppressed;
48 public void GetObjectData(SerializationInfo info, StreamingContext context)
50 throw new PlatformNotSupportedException();
53 public static ExecutionContext Capture()
55 ExecutionContext executionContext = Thread.CurrentThread.ExecutionContext;
57 executionContext == null ? Default :
58 executionContext.m_isFlowSuppressed ? null :
62 private ExecutionContext ShallowClone(bool isFlowSuppressed)
64 Debug.Assert(isFlowSuppressed != m_isFlowSuppressed);
66 if (!isFlowSuppressed &&
67 (m_localValues == null ||
68 m_localValues.GetType() == typeof(AsyncLocalValueMap.EmptyAsyncLocalValueMap))
71 return null; // implies the default context
73 // Flow suppressing a Default context will have null values, set them to Empty
74 return new ExecutionContext(m_localValues ?? AsyncLocalValueMap.Empty, m_localChangeNotifications ?? Array.Empty<IAsyncLocal>(), isFlowSuppressed);
77 public static AsyncFlowControl SuppressFlow()
79 Thread currentThread = Thread.CurrentThread;
80 ExecutionContext executionContext = currentThread.ExecutionContext ?? Default;
81 if (executionContext.m_isFlowSuppressed)
83 throw new InvalidOperationException(SR.InvalidOperation_CannotSupressFlowMultipleTimes);
86 executionContext = executionContext.ShallowClone(isFlowSuppressed: true);
87 var asyncFlowControl = new AsyncFlowControl();
88 currentThread.ExecutionContext = executionContext;
89 asyncFlowControl.Initialize(currentThread);
90 return asyncFlowControl;
93 public static void RestoreFlow()
95 Thread currentThread = Thread.CurrentThread;
96 ExecutionContext executionContext = currentThread.ExecutionContext;
97 if (executionContext == null || !executionContext.m_isFlowSuppressed)
99 throw new InvalidOperationException(SR.InvalidOperation_CannotRestoreUnsupressedFlow);
102 currentThread.ExecutionContext = executionContext.ShallowClone(isFlowSuppressed: false);
105 public static bool IsFlowSuppressed()
107 ExecutionContext executionContext = Thread.CurrentThread.ExecutionContext;
108 return executionContext != null && executionContext.m_isFlowSuppressed;
111 internal bool HasChangeNotifications => m_localChangeNotifications != null;
113 internal bool IsDefault => m_isDefault;
115 public static void Run(ExecutionContext executionContext, ContextCallback callback, Object state)
117 // Note: ExecutionContext.Run is an extremely hot function and used by every await, ThreadPool execution, etc.
118 if (executionContext == null)
123 RunInternal(executionContext, callback, state);
126 internal static void RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
128 // Note: ExecutionContext.RunInternal is an extremely hot function and used by every await, ThreadPool execution, etc.
129 // Note: Manual enregistering may be addressed by "Exception Handling Write Through Optimization"
130 // https://github.com/dotnet/coreclr/blob/master/Documentation/design-docs/eh-writethru.md
132 // Enregister variables with 0 post-fix so they can be used in registers without EH forcing them to stack
133 // Capture references to Thread Contexts
134 Thread currentThread0 = Thread.CurrentThread;
135 Thread currentThread = currentThread0;
136 ExecutionContext previousExecutionCtx0 = currentThread0.ExecutionContext;
138 // Store current ExecutionContext and SynchronizationContext as "previousXxx".
139 // This allows us to restore them and undo any Context changes made in callback.Invoke
140 // so that they won't "leak" back into caller.
141 // These variables will cross EH so be forced to stack
142 ExecutionContext previousExecutionCtx = previousExecutionCtx0;
143 SynchronizationContext previousSyncCtx = currentThread0.SynchronizationContext;
145 if (executionContext != null && executionContext.m_isDefault)
147 // Default is a null ExecutionContext internally
148 executionContext = null;
151 if (previousExecutionCtx0 != executionContext)
153 // Restore changed ExecutionContext
154 currentThread0.ExecutionContext = executionContext;
155 if ((executionContext != null && executionContext.HasChangeNotifications) ||
156 (previousExecutionCtx0 != null && previousExecutionCtx0.HasChangeNotifications))
158 // There are change notifications; trigger any affected
159 OnValuesChanged(previousExecutionCtx0, executionContext);
163 ExceptionDispatchInfo edi = null;
166 callback.Invoke(state);
170 // Note: we have a "catch" rather than a "finally" because we want
171 // to stop the first pass of EH here. That way we can restore the previous
172 // context before any of our callers' EH filters run.
173 edi = ExceptionDispatchInfo.Capture(ex);
176 // Re-enregistrer variables post EH with 1 post-fix so they can be used in registers rather than from stack
177 SynchronizationContext previousSyncCtx1 = previousSyncCtx;
178 Thread currentThread1 = currentThread;
179 // The common case is that these have not changed, so avoid the cost of a write barrier if not needed.
180 if (currentThread1.SynchronizationContext != previousSyncCtx1)
182 // Restore changed SynchronizationContext back to previous
183 currentThread1.SynchronizationContext = previousSyncCtx1;
186 ExecutionContext previousExecutionCtx1 = previousExecutionCtx;
187 ExecutionContext currentExecutionCtx1 = currentThread1.ExecutionContext;
188 if (currentExecutionCtx1 != previousExecutionCtx1)
190 // Restore changed ExecutionContext back to previous
191 currentThread1.ExecutionContext = previousExecutionCtx1;
192 if ((currentExecutionCtx1 != null && currentExecutionCtx1.HasChangeNotifications) ||
193 (previousExecutionCtx1 != null && previousExecutionCtx1.HasChangeNotifications))
195 // There are change notifications; trigger any affected
196 OnValuesChanged(currentExecutionCtx1, previousExecutionCtx1);
200 // If exception was thrown by callback, rethrow it now original contexts are restored
204 internal static void OnValuesChanged(ExecutionContext previousExecutionCtx, ExecutionContext nextExecutionCtx)
206 Debug.Assert(previousExecutionCtx != nextExecutionCtx);
208 // Collect Change Notifications
209 IAsyncLocal[] previousChangeNotifications = previousExecutionCtx?.m_localChangeNotifications;
210 IAsyncLocal[] nextChangeNotifications = nextExecutionCtx?.m_localChangeNotifications;
212 // At least one side must have notifications
213 Debug.Assert(previousChangeNotifications != null || nextChangeNotifications != null);
215 // Fire Change Notifications
218 if (previousChangeNotifications != null && nextChangeNotifications != null)
220 // Notifications can't exist without values
221 Debug.Assert(previousExecutionCtx.m_localValues != null);
222 Debug.Assert(nextExecutionCtx.m_localValues != null);
223 // Both contexts have change notifications, check previousExecutionCtx first
224 foreach (IAsyncLocal local in previousChangeNotifications)
226 previousExecutionCtx.m_localValues.TryGetValue(local, out object previousValue);
227 nextExecutionCtx.m_localValues.TryGetValue(local, out object currentValue);
229 if (previousValue != currentValue)
231 local.OnValueChanged(previousValue, currentValue, contextChanged: true);
235 if (nextChangeNotifications != previousChangeNotifications)
237 // Check for additional notifications in nextExecutionCtx
238 foreach (IAsyncLocal local in nextChangeNotifications)
240 // If the local has a value in the previous context, we already fired the event
241 // for that local in the code above.
242 if (!previousExecutionCtx.m_localValues.TryGetValue(local, out object previousValue))
244 nextExecutionCtx.m_localValues.TryGetValue(local, out object currentValue);
245 if (previousValue != currentValue)
247 local.OnValueChanged(previousValue, currentValue, contextChanged: true);
253 else if (previousChangeNotifications != null)
255 // Notifications can't exist without values
256 Debug.Assert(previousExecutionCtx.m_localValues != null);
257 // No current values, so just check previous against null
258 foreach (IAsyncLocal local in previousChangeNotifications)
260 previousExecutionCtx.m_localValues.TryGetValue(local, out object previousValue);
261 if (previousValue != null)
263 local.OnValueChanged(previousValue, null, contextChanged: true);
267 else // Implied: nextChangeNotifications != null
269 // Notifications can't exist without values
270 Debug.Assert(nextExecutionCtx.m_localValues != null);
271 // No previous values, so just check current against null
272 foreach (IAsyncLocal local in nextChangeNotifications)
274 nextExecutionCtx.m_localValues.TryGetValue(local, out object currentValue);
275 if (currentValue != null)
277 local.OnValueChanged(null, currentValue, contextChanged: true);
284 Environment.FailFast(
285 SR.ExecutionContext_ExceptionInAsyncLocalNotification,
291 private static void ThrowNullContext()
293 throw new InvalidOperationException(SR.InvalidOperation_NullContext);
296 internal static object GetLocalValue(IAsyncLocal local)
298 ExecutionContext current = Thread.CurrentThread.ExecutionContext;
304 current.m_localValues.TryGetValue(local, out object value);
308 internal static void SetLocalValue(IAsyncLocal local, object newValue, bool needChangeNotifications)
310 ExecutionContext current = Thread.CurrentThread.ExecutionContext;
312 object previousValue = null;
313 bool hadPreviousValue = false;
316 hadPreviousValue = current.m_localValues.TryGetValue(local, out previousValue);
319 if (previousValue == newValue)
324 IAsyncLocal[] newChangeNotifications = null;
325 IAsyncLocalValueMap newValues;
326 bool isFlowSuppressed = false;
329 isFlowSuppressed = current.m_isFlowSuppressed;
330 newValues = current.m_localValues.Set(local, newValue);
331 newChangeNotifications = current.m_localChangeNotifications;
336 newValues = new AsyncLocalValueMap.OneElementAsyncLocalValueMap(local, newValue);
340 // Either copy the change notification array, or create a new one, depending on whether we need to add a new item.
342 if (needChangeNotifications)
344 if (hadPreviousValue)
346 Debug.Assert(newChangeNotifications != null);
347 Debug.Assert(Array.IndexOf(newChangeNotifications, local) >= 0);
349 else if (newChangeNotifications == null)
351 newChangeNotifications = new IAsyncLocal[1] { local };
355 int newNotificationIndex = newChangeNotifications.Length;
356 Array.Resize(ref newChangeNotifications, newNotificationIndex + 1);
357 newChangeNotifications[newNotificationIndex] = local;
361 Thread.CurrentThread.ExecutionContext =
362 (!isFlowSuppressed && newValues.GetType() == typeof(AsyncLocalValueMap.EmptyAsyncLocalValueMap)) ?
363 null : // No values, return to Default context
364 new ExecutionContext(newValues, newChangeNotifications, isFlowSuppressed);
366 if (needChangeNotifications)
368 local.OnValueChanged(previousValue, newValue, contextChanged: false);
372 public ExecutionContext CreateCopy()
374 return this; // since CoreCLR's ExecutionContext is immutable, we don't need to create copies.
377 public void Dispose()
379 // For CLR compat only
383 public struct AsyncFlowControl : IDisposable
385 private Thread _thread;
387 internal void Initialize(Thread currentThread)
389 Debug.Assert(currentThread == Thread.CurrentThread);
390 _thread = currentThread;
397 throw new InvalidOperationException(SR.InvalidOperation_CannotUseAFCMultiple);
399 if (Thread.CurrentThread != _thread)
401 throw new InvalidOperationException(SR.InvalidOperation_CannotUseAFCOtherThread);
404 // An async flow control cannot be undone when a different execution context is applied. The desktop framework
405 // mutates the execution context when its state changes, and only changes the instance when an execution context
406 // is applied (for instance, through ExecutionContext.Run). The framework prevents a suppressed-flow execution
407 // context from being applied by returning null from ExecutionContext.Capture, so the only type of execution
408 // context that can be applied is one whose flow is not suppressed. After suppressing flow and changing an async
409 // local's value, the desktop framework verifies that a different execution context has not been applied by
410 // checking the execution context instance against the one saved from when flow was suppressed. In .NET Core,
411 // since the execution context instance will change after changing the async local's value, it verifies that a
412 // different execution context has not been applied, by instead ensuring that the current execution context's
413 // flow is suppressed.
414 if (!ExecutionContext.IsFlowSuppressed())
416 throw new InvalidOperationException(SR.InvalidOperation_AsyncFlowCtrlCtxMismatch);
420 ExecutionContext.RestoreFlow();
423 public void Dispose()
428 public override bool Equals(object obj)
430 return obj is AsyncFlowControl && Equals((AsyncFlowControl)obj);
433 public bool Equals(AsyncFlowControl obj)
435 return _thread == obj._thread;
438 public override int GetHashCode()
440 return _thread?.GetHashCode() ?? 0;
443 public static bool operator ==(AsyncFlowControl a, AsyncFlowControl b)
448 public static bool operator !=(AsyncFlowControl a, AsyncFlowControl b)