Merge pull request #11894 from pgavlin/gh11804
[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.Diagnostics.Contracts;
16 using System.Runtime.ExceptionServices;
17 using System.Runtime.Serialization;
18
19 using Thread = Internal.Runtime.Augments.RuntimeThread;
20
21 namespace System.Threading
22 {
23     public delegate void ContextCallback(Object state);
24
25     internal struct ExecutionContextSwitcher
26     {
27         internal ExecutionContext m_ec;
28         internal SynchronizationContext m_sc;
29
30         internal void Undo(Thread currentThread)
31         {
32             Debug.Assert(currentThread == Thread.CurrentThread);
33
34             // The common case is that these have not changed, so avoid the cost of a write if not needed.
35             if (currentThread.SynchronizationContext != m_sc)
36             {
37                 currentThread.SynchronizationContext = m_sc;
38             }
39
40             if (currentThread.ExecutionContext != m_ec)
41             {
42                 ExecutionContext.Restore(currentThread, m_ec);
43             }
44         }
45     }
46
47     public sealed class ExecutionContext : IDisposable, ISerializable
48     {
49         internal static readonly ExecutionContext Default = new ExecutionContext();
50
51         private readonly IAsyncLocalValueMap m_localValues;
52         private readonly IAsyncLocal[] m_localChangeNotifications;
53         private readonly bool m_isFlowSuppressed;
54
55         private ExecutionContext()
56         {
57             m_localValues = AsyncLocalValueMap.Empty;
58             m_localChangeNotifications = Array.Empty<IAsyncLocal>();
59         }
60
61         private ExecutionContext(
62             IAsyncLocalValueMap localValues,
63             IAsyncLocal[] localChangeNotifications,
64             bool isFlowSuppressed)
65         {
66             m_localValues = localValues;
67             m_localChangeNotifications = localChangeNotifications;
68             m_isFlowSuppressed = isFlowSuppressed;
69         }
70
71         public void GetObjectData(SerializationInfo info, StreamingContext context)
72         {
73             if (info == null)
74             {
75                 throw new ArgumentNullException(nameof(info));
76             }
77             Contract.EndContractBlock();
78         }
79
80         private ExecutionContext(SerializationInfo info, StreamingContext context)
81         {
82         }
83
84         public static ExecutionContext Capture()
85         {
86             ExecutionContext executionContext = Thread.CurrentThread.ExecutionContext;
87             return
88                 executionContext == null ? Default :
89                 executionContext.m_isFlowSuppressed ? null :
90                 executionContext;
91         }
92
93         private ExecutionContext ShallowClone(bool isFlowSuppressed)
94         {
95             Debug.Assert(isFlowSuppressed != m_isFlowSuppressed);
96
97             if (!isFlowSuppressed &&
98                 m_localValues == Default.m_localValues &&
99                 m_localChangeNotifications == Default.m_localChangeNotifications)
100             {
101                 return null; // implies the default context
102             }
103             return new ExecutionContext(m_localValues, m_localChangeNotifications, isFlowSuppressed);
104         }
105
106         public static AsyncFlowControl SuppressFlow()
107         {
108             Thread currentThread = Thread.CurrentThread;
109             ExecutionContext executionContext = currentThread.ExecutionContext ?? Default;
110             if (executionContext.m_isFlowSuppressed)
111             {
112                 throw new InvalidOperationException(SR.InvalidOperation_CannotSupressFlowMultipleTimes);
113             }
114             Contract.EndContractBlock();
115
116             executionContext = executionContext.ShallowClone(isFlowSuppressed: true);
117             var asyncFlowControl = new AsyncFlowControl();
118             currentThread.ExecutionContext = executionContext;
119             asyncFlowControl.Initialize(currentThread);
120             return asyncFlowControl;
121         }
122
123         public static void RestoreFlow()
124         {
125             Thread currentThread = Thread.CurrentThread;
126             ExecutionContext executionContext = currentThread.ExecutionContext;
127             if (executionContext == null || !executionContext.m_isFlowSuppressed)
128             {
129                 throw new InvalidOperationException(SR.InvalidOperation_CannotRestoreUnsupressedFlow);
130             }
131             Contract.EndContractBlock();
132
133             currentThread.ExecutionContext = executionContext.ShallowClone(isFlowSuppressed: false);
134         }
135
136         public static bool IsFlowSuppressed()
137         {
138             ExecutionContext executionContext = Thread.CurrentThread.ExecutionContext;
139             return executionContext != null && executionContext.m_isFlowSuppressed;
140         }
141
142         public static void Run(ExecutionContext executionContext, ContextCallback callback, Object state)
143         {
144             if (executionContext == null)
145                 throw new InvalidOperationException(SR.InvalidOperation_NullContext);
146
147             Thread currentThread = Thread.CurrentThread;
148             ExecutionContextSwitcher ecsw = default(ExecutionContextSwitcher);
149             try
150             {
151                 EstablishCopyOnWriteScope(currentThread, ref ecsw);
152                 ExecutionContext.Restore(currentThread, executionContext);
153                 callback(state);
154             }
155             catch
156             {
157                 // Note: we have a "catch" rather than a "finally" because we want
158                 // to stop the first pass of EH here.  That way we can restore the previous
159                 // context before any of our callers' EH filters run.  That means we need to
160                 // end the scope separately in the non-exceptional case below.
161                 ecsw.Undo(currentThread);
162                 throw;
163             }
164             ecsw.Undo(currentThread);
165         }
166
167         internal static void Restore(Thread currentThread, ExecutionContext executionContext)
168         {
169             Debug.Assert(currentThread == Thread.CurrentThread);
170
171             ExecutionContext previous = currentThread.ExecutionContext ?? Default;
172             currentThread.ExecutionContext = executionContext;
173
174             // New EC could be null if that's what ECS.Undo saved off.
175             // For the purposes of dealing with context change, treat this as the default EC
176             executionContext = executionContext ?? Default;
177
178             if (previous != executionContext)
179             {
180                 OnContextChanged(previous, executionContext);
181             }
182         }
183
184         internal static void EstablishCopyOnWriteScope(Thread currentThread, ref ExecutionContextSwitcher ecsw)
185         {
186             Debug.Assert(currentThread == Thread.CurrentThread);
187
188             ecsw.m_ec = currentThread.ExecutionContext;
189             ecsw.m_sc = currentThread.SynchronizationContext;
190         }
191
192         private static void OnContextChanged(ExecutionContext previous, ExecutionContext current)
193         {
194             Debug.Assert(previous != null);
195             Debug.Assert(current != null);
196             Debug.Assert(previous != current);
197
198             foreach (IAsyncLocal local in previous.m_localChangeNotifications)
199             {
200                 object previousValue;
201                 object currentValue;
202                 previous.m_localValues.TryGetValue(local, out previousValue);
203                 current.m_localValues.TryGetValue(local, out currentValue);
204
205                 if (previousValue != currentValue)
206                     local.OnValueChanged(previousValue, currentValue, true);
207             }
208
209             if (current.m_localChangeNotifications != previous.m_localChangeNotifications)
210             {
211                 try
212                 {
213                     foreach (IAsyncLocal local in current.m_localChangeNotifications)
214                     {
215                         // If the local has a value in the previous context, we already fired the event for that local
216                         // in the code above.
217                         object previousValue;
218                         if (!previous.m_localValues.TryGetValue(local, out previousValue))
219                         {
220                             object currentValue;
221                             current.m_localValues.TryGetValue(local, out currentValue);
222
223                             if (previousValue != currentValue)
224                                 local.OnValueChanged(previousValue, currentValue, true);
225                         }
226                     }
227                 }
228                 catch (Exception ex)
229                 {
230                     Environment.FailFast(
231                         SR.ExecutionContext_ExceptionInAsyncLocalNotification,
232                         ex);
233                 }
234             }
235         }
236
237         internal static object GetLocalValue(IAsyncLocal local)
238         {
239             ExecutionContext current = Thread.CurrentThread.ExecutionContext;
240             if (current == null)
241                 return null;
242
243             object value;
244             current.m_localValues.TryGetValue(local, out value);
245             return value;
246         }
247
248         internal static void SetLocalValue(IAsyncLocal local, object newValue, bool needChangeNotifications)
249         {
250             ExecutionContext current = Thread.CurrentThread.ExecutionContext ?? ExecutionContext.Default;
251
252             object previousValue;
253             bool hadPreviousValue = current.m_localValues.TryGetValue(local, out previousValue);
254
255             if (previousValue == newValue)
256                 return;
257
258             IAsyncLocalValueMap newValues = current.m_localValues.Set(local, newValue);
259
260             //
261             // Either copy the change notification array, or create a new one, depending on whether we need to add a new item.
262             //
263             IAsyncLocal[] newChangeNotifications = current.m_localChangeNotifications;
264             if (needChangeNotifications)
265             {
266                 if (hadPreviousValue)
267                 {
268                     Debug.Assert(Array.IndexOf(newChangeNotifications, local) >= 0);
269                 }
270                 else
271                 {
272                     int newNotificationIndex = newChangeNotifications.Length;
273                     Array.Resize(ref newChangeNotifications, newNotificationIndex + 1);
274                     newChangeNotifications[newNotificationIndex] = local;
275                 }
276             }
277
278             Thread.CurrentThread.ExecutionContext =
279                 new ExecutionContext(newValues, newChangeNotifications, current.m_isFlowSuppressed);
280
281             if (needChangeNotifications)
282             {
283                 local.OnValueChanged(previousValue, newValue, false);
284             }
285         }
286
287         public ExecutionContext CreateCopy()
288         {
289             return this; // since CoreCLR's ExecutionContext is immutable, we don't need to create copies.
290         }
291
292         public void Dispose()
293         {
294             // For CLR compat only
295         }
296     }
297
298     public struct AsyncFlowControl : IDisposable
299     {
300         private Thread _thread;
301
302         internal void Initialize(Thread currentThread)
303         {
304             Debug.Assert(currentThread == Thread.CurrentThread);
305             _thread = currentThread;
306         }
307
308         public void Undo()
309         {
310             if (_thread == null)
311             {
312                 throw new InvalidOperationException(SR.InvalidOperation_CannotUseAFCMultiple);
313             }
314             if (Thread.CurrentThread != _thread)
315             {
316                 throw new InvalidOperationException(SR.InvalidOperation_CannotUseAFCOtherThread);
317             }
318
319             // An async flow control cannot be undone when a different execution context is applied. The desktop framework
320             // mutates the execution context when its state changes, and only changes the instance when an execution context
321             // is applied (for instance, through ExecutionContext.Run). The framework prevents a suppressed-flow execution
322             // context from being applied by returning null from ExecutionContext.Capture, so the only type of execution
323             // context that can be applied is one whose flow is not suppressed. After suppressing flow and changing an async
324             // local's value, the desktop framework verifies that a different execution context has not been applied by
325             // checking the execution context instance against the one saved from when flow was suppressed. In .NET Core,
326             // since the execution context instance will change after changing the async local's value, it verifies that a
327             // different execution context has not been applied, by instead ensuring that the current execution context's
328             // flow is suppressed.
329             if (!ExecutionContext.IsFlowSuppressed())
330             {
331                 throw new InvalidOperationException(SR.InvalidOperation_AsyncFlowCtrlCtxMismatch);
332             }
333             Contract.EndContractBlock();
334
335             _thread = null;
336             ExecutionContext.RestoreFlow();
337         }
338
339         public void Dispose()
340         {
341             Undo();
342         }
343
344         public override bool Equals(object obj)
345         {
346             return obj is AsyncFlowControl && Equals((AsyncFlowControl)obj);
347         }
348
349         public bool Equals(AsyncFlowControl obj)
350         {
351             return _thread == obj._thread;
352         }
353
354         public override int GetHashCode()
355         {
356             return _thread?.GetHashCode() ?? 0;
357         }
358
359         public static bool operator ==(AsyncFlowControl a, AsyncFlowControl b)
360         {
361             return a.Equals(b);
362         }
363
364         public static bool operator !=(AsyncFlowControl a, AsyncFlowControl b)
365         {
366             return !(a == b);
367         }
368     }
369 }