Reduce Execution Context Save+Restore (#15629)
[platform/upstream/coreclr.git] / src / mscorlib / shared / System / Threading / ExecutionContext.cs
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.
4
5 /*============================================================
6 **
7 **
8 **
9 ** Purpose: Capture execution  context for a thread
10 **
11 **
12 ===========================================================*/
13
14 using System.Diagnostics;
15 using System.Runtime.ExceptionServices;
16 using System.Runtime.Serialization;
17
18 using Thread = Internal.Runtime.Augments.RuntimeThread;
19
20 namespace System.Threading
21 {
22     public delegate void ContextCallback(Object state);
23
24     public sealed class ExecutionContext : IDisposable, ISerializable
25     {
26         internal static readonly ExecutionContext Default = new ExecutionContext(isDefault: true);
27
28         private readonly IAsyncLocalValueMap m_localValues;
29         private readonly IAsyncLocal[] m_localChangeNotifications;
30         private readonly bool m_isFlowSuppressed;
31         private readonly bool m_isDefault;
32
33         private ExecutionContext(bool isDefault)
34         {
35             m_isDefault = isDefault;
36         }
37
38         private ExecutionContext(
39             IAsyncLocalValueMap localValues,
40             IAsyncLocal[] localChangeNotifications,
41             bool isFlowSuppressed)
42         {
43             m_localValues = localValues;
44             m_localChangeNotifications = localChangeNotifications;
45             m_isFlowSuppressed = isFlowSuppressed;
46         }
47
48         public void GetObjectData(SerializationInfo info, StreamingContext context)
49         {
50             throw new PlatformNotSupportedException();
51         }
52
53         public static ExecutionContext Capture()
54         {
55             ExecutionContext executionContext = Thread.CurrentThread.ExecutionContext;
56             return
57                 executionContext == null ? Default :
58                 executionContext.m_isFlowSuppressed ? null :
59                 executionContext;
60         }
61
62         private ExecutionContext ShallowClone(bool isFlowSuppressed)
63         {
64             Debug.Assert(isFlowSuppressed != m_isFlowSuppressed);
65
66             if (!isFlowSuppressed &&
67                 (m_localValues == null ||
68                  m_localValues.GetType() == typeof(AsyncLocalValueMap.EmptyAsyncLocalValueMap))
69                )
70             {
71                 return null; // implies the default context
72             }
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);
75         }
76
77         public static AsyncFlowControl SuppressFlow()
78         {
79             Thread currentThread = Thread.CurrentThread;
80             ExecutionContext executionContext = currentThread.ExecutionContext ?? Default;
81             if (executionContext.m_isFlowSuppressed)
82             {
83                 throw new InvalidOperationException(SR.InvalidOperation_CannotSupressFlowMultipleTimes);
84             }
85
86             executionContext = executionContext.ShallowClone(isFlowSuppressed: true);
87             var asyncFlowControl = new AsyncFlowControl();
88             currentThread.ExecutionContext = executionContext;
89             asyncFlowControl.Initialize(currentThread);
90             return asyncFlowControl;
91         }
92
93         public static void RestoreFlow()
94         {
95             Thread currentThread = Thread.CurrentThread;
96             ExecutionContext executionContext = currentThread.ExecutionContext;
97             if (executionContext == null || !executionContext.m_isFlowSuppressed)
98             {
99                 throw new InvalidOperationException(SR.InvalidOperation_CannotRestoreUnsupressedFlow);
100             }
101
102             currentThread.ExecutionContext = executionContext.ShallowClone(isFlowSuppressed: false);
103         }
104
105         public static bool IsFlowSuppressed()
106         {
107             ExecutionContext executionContext = Thread.CurrentThread.ExecutionContext;
108             return executionContext != null && executionContext.m_isFlowSuppressed;
109         }
110
111         internal bool HasChangeNotifications => m_localChangeNotifications != null;
112
113         internal bool IsDefault => m_isDefault;
114
115         public static void Run(ExecutionContext executionContext, ContextCallback callback, Object state)
116         {
117             // Note: ExecutionContext.Run is an extremely hot function and used by every await, ThreadPool execution, etc.
118             if (executionContext == null)
119             {
120                 ThrowNullContext();
121             }
122
123             RunInternal(executionContext, callback, state);
124         }
125
126         internal static void RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
127         {
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
131
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;
137
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;
144
145             if (executionContext != null && executionContext.m_isDefault)
146             {
147                 // Default is a null ExecutionContext internally
148                 executionContext = null;
149             }
150
151             if (previousExecutionCtx0 != executionContext)
152             {
153                 // Restore changed ExecutionContext
154                 currentThread0.ExecutionContext = executionContext;
155                 if ((executionContext != null && executionContext.HasChangeNotifications) ||
156                     (previousExecutionCtx0 != null && previousExecutionCtx0.HasChangeNotifications))
157                 {
158                     // There are change notifications; trigger any affected
159                     OnValuesChanged(previousExecutionCtx0, executionContext);
160                 }
161             }
162
163             ExceptionDispatchInfo edi = null;
164             try
165             {
166                 callback.Invoke(state);
167             }
168             catch (Exception ex)
169             {
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);
174             }
175
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)
181             {
182                 // Restore changed SynchronizationContext back to previous
183                 currentThread1.SynchronizationContext = previousSyncCtx1;
184             }
185
186             ExecutionContext previousExecutionCtx1 = previousExecutionCtx;
187             ExecutionContext currentExecutionCtx1 = currentThread1.ExecutionContext;
188             if (currentExecutionCtx1 != previousExecutionCtx1)
189             {
190                 // Restore changed ExecutionContext back to previous
191                 currentThread1.ExecutionContext = previousExecutionCtx1;
192                 if ((currentExecutionCtx1 != null && currentExecutionCtx1.HasChangeNotifications) ||
193                     (previousExecutionCtx1 != null && previousExecutionCtx1.HasChangeNotifications))
194                 {
195                     // There are change notifications; trigger any affected
196                     OnValuesChanged(currentExecutionCtx1, previousExecutionCtx1);
197                 }
198             }
199
200             // If exception was thrown by callback, rethrow it now original contexts are restored
201             edi?.Throw();
202         }
203
204         internal static void OnValuesChanged(ExecutionContext previousExecutionCtx, ExecutionContext nextExecutionCtx)
205         {
206             Debug.Assert(previousExecutionCtx != nextExecutionCtx);
207
208             // Collect Change Notifications 
209             IAsyncLocal[] previousChangeNotifications = previousExecutionCtx?.m_localChangeNotifications;
210             IAsyncLocal[] nextChangeNotifications = nextExecutionCtx?.m_localChangeNotifications;
211
212             // At least one side must have notifications
213             Debug.Assert(previousChangeNotifications != null || nextChangeNotifications != null);
214
215             // Fire Change Notifications
216             try
217             {
218                 if (previousChangeNotifications != null && nextChangeNotifications != null)
219                 {
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)
225                     {
226                         previousExecutionCtx.m_localValues.TryGetValue(local, out object previousValue);
227                         nextExecutionCtx.m_localValues.TryGetValue(local, out object currentValue);
228
229                         if (previousValue != currentValue)
230                         {
231                             local.OnValueChanged(previousValue, currentValue, contextChanged: true);
232                         }
233                     }
234
235                     if (nextChangeNotifications != previousChangeNotifications)
236                     {
237                         // Check for additional notifications in nextExecutionCtx
238                         foreach (IAsyncLocal local in nextChangeNotifications)
239                         {
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))
243                             {
244                                 nextExecutionCtx.m_localValues.TryGetValue(local, out object currentValue);
245                                 if (previousValue != currentValue)
246                                 {
247                                     local.OnValueChanged(previousValue, currentValue, contextChanged: true);
248                                 }
249                             }
250                         }
251                     }
252                 }
253                 else if (previousChangeNotifications != null)
254                 {
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)
259                     {
260                         previousExecutionCtx.m_localValues.TryGetValue(local, out object previousValue);
261                         if (previousValue != null)
262                         {
263                             local.OnValueChanged(previousValue, null, contextChanged: true);
264                         }
265                     }
266                 }
267                 else // Implied: nextChangeNotifications != null
268                 {
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)
273                     {
274                         nextExecutionCtx.m_localValues.TryGetValue(local, out object currentValue);
275                         if (currentValue != null)
276                         {
277                             local.OnValueChanged(null, currentValue, contextChanged: true);
278                         }
279                     }
280                 }
281             }
282             catch (Exception ex)
283             {
284                 Environment.FailFast(
285                     SR.ExecutionContext_ExceptionInAsyncLocalNotification,
286                     ex);
287             }
288         }
289
290         [StackTraceHidden]
291         private static void ThrowNullContext()
292         {
293             throw new InvalidOperationException(SR.InvalidOperation_NullContext);
294         }
295
296         internal static object GetLocalValue(IAsyncLocal local)
297         {
298             ExecutionContext current = Thread.CurrentThread.ExecutionContext;
299             if (current == null)
300             {
301                 return null;
302             }
303
304             current.m_localValues.TryGetValue(local, out object value);
305             return value;
306         }
307
308         internal static void SetLocalValue(IAsyncLocal local, object newValue, bool needChangeNotifications)
309         {
310             ExecutionContext current = Thread.CurrentThread.ExecutionContext;
311
312             object previousValue = null;
313             bool hadPreviousValue = false;
314             if (current != null)
315             {
316                 hadPreviousValue = current.m_localValues.TryGetValue(local, out previousValue);
317             }
318
319             if (previousValue == newValue)
320             {
321                 return;
322             }
323
324             IAsyncLocal[] newChangeNotifications = null;
325             IAsyncLocalValueMap newValues;
326             bool isFlowSuppressed = false;
327             if (current != null)
328             {
329                 isFlowSuppressed = current.m_isFlowSuppressed;
330                 newValues = current.m_localValues.Set(local, newValue);
331                 newChangeNotifications = current.m_localChangeNotifications;
332             }
333             else
334             {
335                 // First AsyncLocal
336                 newValues = new AsyncLocalValueMap.OneElementAsyncLocalValueMap(local, newValue);
337             }
338
339             //
340             // Either copy the change notification array, or create a new one, depending on whether we need to add a new item.
341             //
342             if (needChangeNotifications)
343             {
344                 if (hadPreviousValue)
345                 {
346                     Debug.Assert(newChangeNotifications != null);
347                     Debug.Assert(Array.IndexOf(newChangeNotifications, local) >= 0);
348                 }
349                 else if (newChangeNotifications == null)
350                 {
351                     newChangeNotifications = new IAsyncLocal[1] { local };
352                 }
353                 else
354                 {
355                     int newNotificationIndex = newChangeNotifications.Length;
356                     Array.Resize(ref newChangeNotifications, newNotificationIndex + 1);
357                     newChangeNotifications[newNotificationIndex] = local;
358                 }
359             }
360
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);
365
366             if (needChangeNotifications)
367             {
368                 local.OnValueChanged(previousValue, newValue, contextChanged: false);
369             }
370         }
371
372         public ExecutionContext CreateCopy()
373         {
374             return this; // since CoreCLR's ExecutionContext is immutable, we don't need to create copies.
375         }
376
377         public void Dispose()
378         {
379             // For CLR compat only
380         }
381     }
382
383     public struct AsyncFlowControl : IDisposable
384     {
385         private Thread _thread;
386
387         internal void Initialize(Thread currentThread)
388         {
389             Debug.Assert(currentThread == Thread.CurrentThread);
390             _thread = currentThread;
391         }
392
393         public void Undo()
394         {
395             if (_thread == null)
396             {
397                 throw new InvalidOperationException(SR.InvalidOperation_CannotUseAFCMultiple);
398             }
399             if (Thread.CurrentThread != _thread)
400             {
401                 throw new InvalidOperationException(SR.InvalidOperation_CannotUseAFCOtherThread);
402             }
403
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())
415             {
416                 throw new InvalidOperationException(SR.InvalidOperation_AsyncFlowCtrlCtxMismatch);
417             }
418
419             _thread = null;
420             ExecutionContext.RestoreFlow();
421         }
422
423         public void Dispose()
424         {
425             Undo();
426         }
427
428         public override bool Equals(object obj)
429         {
430             return obj is AsyncFlowControl && Equals((AsyncFlowControl)obj);
431         }
432
433         public bool Equals(AsyncFlowControl obj)
434         {
435             return _thread == obj._thread;
436         }
437
438         public override int GetHashCode()
439         {
440             return _thread?.GetHashCode() ?? 0;
441         }
442
443         public static bool operator ==(AsyncFlowControl a, AsyncFlowControl b)
444         {
445             return a.Equals(b);
446         }
447
448         public static bool operator !=(AsyncFlowControl a, AsyncFlowControl b)
449         {
450             return !(a == b);
451         }
452     }
453 }