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 using System
.Collections
.Generic
;
6 using System
.Diagnostics
;
7 using System
.Diagnostics
.CodeAnalysis
;
9 namespace System
.Threading
12 // AsyncLocal<T> represents "ambient" data that is local to a given asynchronous control flow, such as an
13 // async method. For example, say you want to associate a culture with a given async flow:
15 // static AsyncLocal<Culture> s_currentCulture = new AsyncLocal<Culture>();
17 // static async Task SomeOperationAsync(Culture culture)
19 // s_currentCulture.Value = culture;
24 // static async Task FooAsync()
26 // PrintStringWithCulture(s_currentCulture.Value);
29 // AsyncLocal<T> also provides optional notifications when the value associated with the current thread
30 // changes, either because it was explicitly changed by setting the Value property, or implicitly changed
31 // when the thread encountered an "await" or other context transition. For example, we might want our
32 // current culture to be communicated to the OS as well:
34 // static AsyncLocal<Culture> s_currentCulture = new AsyncLocal<Culture>(
37 // NativeMethods.SetThreadCulture(args.CurrentValue.LCID);
40 public sealed class AsyncLocal
<T
> : IAsyncLocal
42 private readonly Action
<AsyncLocalValueChangedArgs
<T
>>? m_valueChangedHandler
;
45 // Constructs an AsyncLocal<T> that does not receive change notifications.
52 // Constructs an AsyncLocal<T> with a delegate that is called whenever the current value changes
55 public AsyncLocal(Action
<AsyncLocalValueChangedArgs
<T
>>? valueChangedHandler
)
57 m_valueChangedHandler
= valueChangedHandler
;
65 object? obj
= ExecutionContext
.GetLocalValue(this);
66 return (obj
== null) ? default : (T
)obj
;
70 ExecutionContext
.SetLocalValue(this, value, m_valueChangedHandler
!= null);
74 void IAsyncLocal
.OnValueChanged(object? previousValueObj
, object? currentValueObj
, bool contextChanged
)
76 Debug
.Assert(m_valueChangedHandler
!= null);
77 T previousValue
= previousValueObj
== null ? default! : (T
)previousValueObj
;
78 T currentValue
= currentValueObj
== null ? default! : (T
)currentValueObj
;
79 m_valueChangedHandler(new AsyncLocalValueChangedArgs
<T
>(previousValue
, currentValue
, contextChanged
));
84 // Interface to allow non-generic code in ExecutionContext to call into the generic AsyncLocal<T> type.
86 internal interface IAsyncLocal
88 void OnValueChanged(object? previousValue
, object? currentValue
, bool contextChanged
);
91 public readonly struct AsyncLocalValueChangedArgs
<T
>
93 [MaybeNull
] public T PreviousValue { get; }
94 [MaybeNull
] public T CurrentValue { get; }
97 // If the value changed because we changed to a different ExecutionContext, this is true. If it changed
98 // because someone set the Value property, this is false.
100 public bool ThreadContextChanged { get; }
102 internal AsyncLocalValueChangedArgs([AllowNull
] T previousValue
, [AllowNull
] T currentValue
, bool contextChanged
)
104 PreviousValue
= previousValue
;
105 CurrentValue
= currentValue
;
106 ThreadContextChanged
= contextChanged
;
111 // Interface used to store an IAsyncLocal => object mapping in ExecutionContext.
112 // Implementations are specialized based on the number of elements in the immutable
113 // map in order to minimize memory consumption and look-up times.
115 internal interface IAsyncLocalValueMap
117 bool TryGetValue(IAsyncLocal key
, out object? value);
118 IAsyncLocalValueMap
Set(IAsyncLocal key
, object? value, bool treatNullValueAsNonexistent
);
122 // Utility functions for getting/creating instances of IAsyncLocalValueMap
124 internal static class AsyncLocalValueMap
126 public static IAsyncLocalValueMap Empty { get; }
= new EmptyAsyncLocalValueMap();
128 public static bool IsEmpty(IAsyncLocalValueMap asyncLocalValueMap
)
130 Debug
.Assert(asyncLocalValueMap
!= null);
131 Debug
.Assert(asyncLocalValueMap
== Empty
|| asyncLocalValueMap
.GetType() != typeof(EmptyAsyncLocalValueMap
));
133 return asyncLocalValueMap
== Empty
;
136 public static IAsyncLocalValueMap
Create(IAsyncLocal key
, object? value, bool treatNullValueAsNonexistent
)
138 // If the value isn't null or a null value may not be treated as nonexistent, then create a new one-element map
139 // to store the key/value pair. Otherwise, use the empty map.
140 return value != null || !treatNullValueAsNonexistent
?
141 new OneElementAsyncLocalValueMap(key
, value) :
145 // Instance without any key/value pairs. Used as a singleton/
146 private sealed class EmptyAsyncLocalValueMap
: IAsyncLocalValueMap
148 public IAsyncLocalValueMap
Set(IAsyncLocal key
, object? value, bool treatNullValueAsNonexistent
)
150 // If the value isn't null or a null value may not be treated as nonexistent, then create a new one-element map
151 // to store the key/value pair. Otherwise, use the empty map.
152 return value != null || !treatNullValueAsNonexistent
?
153 new OneElementAsyncLocalValueMap(key
, value) :
154 (IAsyncLocalValueMap
)this;
157 public bool TryGetValue(IAsyncLocal key
, out object? value)
164 // Instance with one key/value pair.
165 private sealed class OneElementAsyncLocalValueMap
: IAsyncLocalValueMap
167 private readonly IAsyncLocal _key1
;
168 private readonly object? _value1
;
170 public OneElementAsyncLocalValueMap(IAsyncLocal key
, object? value)
172 _key1
= key
; _value1
= value;
175 public IAsyncLocalValueMap
Set(IAsyncLocal key
, object? value, bool treatNullValueAsNonexistent
)
177 if (value != null || !treatNullValueAsNonexistent
)
179 // If the key matches one already contained in this map, then create a new one-element map with the updated
180 // value, otherwise create a two-element map with the additional key/value.
181 return ReferenceEquals(key
, _key1
) ?
182 new OneElementAsyncLocalValueMap(key
, value) :
183 (IAsyncLocalValueMap
)new TwoElementAsyncLocalValueMap(_key1
, _value1
, key
, value);
187 // If the key exists in this map, remove it by downgrading to an empty map. Otherwise, there's nothing to
188 // add or remove, so just return this map.
189 return ReferenceEquals(key
, _key1
) ?
191 (IAsyncLocalValueMap
)this;
195 public bool TryGetValue(IAsyncLocal key
, out object? value)
197 if (ReferenceEquals(key
, _key1
))
210 // Instance with two key/value pairs.
211 private sealed class TwoElementAsyncLocalValueMap
: IAsyncLocalValueMap
213 private readonly IAsyncLocal _key1
, _key2
;
214 private readonly object? _value1
, _value2
;
216 public TwoElementAsyncLocalValueMap(IAsyncLocal key1
, object? value1
, IAsyncLocal key2
, object? value2
)
218 _key1
= key1
; _value1
= value1
;
219 _key2
= key2
; _value2
= value2
;
222 public IAsyncLocalValueMap
Set(IAsyncLocal key
, object? value, bool treatNullValueAsNonexistent
)
224 if (value != null || !treatNullValueAsNonexistent
)
226 // If the key matches one already contained in this map, then create a new two-element map with the updated
227 // value, otherwise create a three-element map with the additional key/value.
229 ReferenceEquals(key
, _key1
) ? new TwoElementAsyncLocalValueMap(key
, value, _key2
, _value2
) :
230 ReferenceEquals(key
, _key2
) ? new TwoElementAsyncLocalValueMap(_key1
, _value1
, key
, value) :
231 (IAsyncLocalValueMap
)new ThreeElementAsyncLocalValueMap(_key1
, _value1
, _key2
, _value2
, key
, value);
235 // If the key exists in this map, remove it by downgrading to a one-element map without the key. Otherwise,
236 // there's nothing to add or remove, so just return this map.
238 ReferenceEquals(key
, _key1
) ? new OneElementAsyncLocalValueMap(_key2
, _value2
) :
239 ReferenceEquals(key
, _key2
) ? new OneElementAsyncLocalValueMap(_key1
, _value1
) :
240 (IAsyncLocalValueMap
)this;
244 public bool TryGetValue(IAsyncLocal key
, out object? value)
246 if (ReferenceEquals(key
, _key1
))
251 else if (ReferenceEquals(key
, _key2
))
264 // Instance with three key/value pairs.
265 private sealed class ThreeElementAsyncLocalValueMap
: IAsyncLocalValueMap
267 private readonly IAsyncLocal _key1
, _key2
, _key3
;
268 private readonly object? _value1
, _value2
, _value3
;
270 public ThreeElementAsyncLocalValueMap(IAsyncLocal key1
, object? value1
, IAsyncLocal key2
, object? value2
, IAsyncLocal key3
, object? value3
)
272 _key1
= key1
; _value1
= value1
;
273 _key2
= key2
; _value2
= value2
;
274 _key3
= key3
; _value3
= value3
;
277 public IAsyncLocalValueMap
Set(IAsyncLocal key
, object? value, bool treatNullValueAsNonexistent
)
279 if (value != null || !treatNullValueAsNonexistent
)
281 // If the key matches one already contained in this map, then create a new three-element map with the
283 if (ReferenceEquals(key
, _key1
)) return new ThreeElementAsyncLocalValueMap(key
, value, _key2
, _value2
, _key3
, _value3
);
284 if (ReferenceEquals(key
, _key2
)) return new ThreeElementAsyncLocalValueMap(_key1
, _value1
, key
, value, _key3
, _value3
);
285 if (ReferenceEquals(key
, _key3
)) return new ThreeElementAsyncLocalValueMap(_key1
, _value1
, _key2
, _value2
, key
, value);
287 // The key doesn't exist in this map, so upgrade to a multi map that contains
288 // the additional key/value pair.
289 var multi
= new MultiElementAsyncLocalValueMap(4);
290 multi
.UnsafeStore(0, _key1
, _value1
);
291 multi
.UnsafeStore(1, _key2
, _value2
);
292 multi
.UnsafeStore(2, _key3
, _value3
);
293 multi
.UnsafeStore(3, key
, value);
298 // If the key exists in this map, remove it by downgrading to a two-element map without the key. Otherwise,
299 // there's nothing to add or remove, so just return this map.
301 ReferenceEquals(key
, _key1
) ? new TwoElementAsyncLocalValueMap(_key2
, _value2
, _key3
, _value3
) :
302 ReferenceEquals(key
, _key2
) ? new TwoElementAsyncLocalValueMap(_key1
, _value1
, _key3
, _value3
) :
303 ReferenceEquals(key
, _key3
) ? new TwoElementAsyncLocalValueMap(_key1
, _value1
, _key2
, _value2
) :
304 (IAsyncLocalValueMap
)this;
308 public bool TryGetValue(IAsyncLocal key
, out object? value)
310 if (ReferenceEquals(key
, _key1
))
315 else if (ReferenceEquals(key
, _key2
))
320 else if (ReferenceEquals(key
, _key3
))
333 // Instance with up to 16 key/value pairs.
334 private sealed class MultiElementAsyncLocalValueMap
: IAsyncLocalValueMap
336 internal const int MaxMultiElements
= 16;
337 private readonly KeyValuePair
<IAsyncLocal
, object?>[] _keyValues
;
339 internal MultiElementAsyncLocalValueMap(int count
)
341 Debug
.Assert(count
<= MaxMultiElements
);
342 _keyValues
= new KeyValuePair
<IAsyncLocal
, object?>[count
];
345 internal void UnsafeStore(int index
, IAsyncLocal key
, object? value)
347 Debug
.Assert(index
< _keyValues
.Length
);
348 _keyValues
[index
] = new KeyValuePair
<IAsyncLocal
, object?>(key
, value);
351 public IAsyncLocalValueMap
Set(IAsyncLocal key
, object? value, bool treatNullValueAsNonexistent
)
353 // Find the key in this map.
354 for (int i
= 0; i
< _keyValues
.Length
; i
++)
356 if (ReferenceEquals(key
, _keyValues
[i
].Key
))
358 // The key is in the map.
359 if (value != null || !treatNullValueAsNonexistent
)
361 // Create a new map of the same size that has all of the same pairs, with this new key/value pair
362 // overwriting the old.
363 var multi
= new MultiElementAsyncLocalValueMap(_keyValues
.Length
);
364 Array
.Copy(_keyValues
, 0, multi
._keyValues
, 0, _keyValues
.Length
);
365 multi
._keyValues
[i
] = new KeyValuePair
<IAsyncLocal
, object?>(key
, value);
368 else if (_keyValues
.Length
== 4)
370 // We only have four elements, one of which we're removing, so downgrade to a three-element map,
371 // without the matching element.
373 i
== 0 ? new ThreeElementAsyncLocalValueMap(_keyValues
[1].Key
, _keyValues
[1].Value
, _keyValues
[2].Key
, _keyValues
[2].Value
, _keyValues
[3].Key
, _keyValues
[3].Value
) :
374 i
== 1 ? new ThreeElementAsyncLocalValueMap(_keyValues
[0].Key
, _keyValues
[0].Value
, _keyValues
[2].Key
, _keyValues
[2].Value
, _keyValues
[3].Key
, _keyValues
[3].Value
) :
375 i
== 2 ? new ThreeElementAsyncLocalValueMap(_keyValues
[0].Key
, _keyValues
[0].Value
, _keyValues
[1].Key
, _keyValues
[1].Value
, _keyValues
[3].Key
, _keyValues
[3].Value
) :
376 (IAsyncLocalValueMap
)new ThreeElementAsyncLocalValueMap(_keyValues
[0].Key
, _keyValues
[0].Value
, _keyValues
[1].Key
, _keyValues
[1].Value
, _keyValues
[2].Key
, _keyValues
[2].Value
);
380 // We have enough elements remaining to warrant a multi map. Create a new one and copy all of the
381 // elements from this one, except the one to be removed.
382 var multi
= new MultiElementAsyncLocalValueMap(_keyValues
.Length
- 1);
383 if (i
!= 0) Array
.Copy(_keyValues
, 0, multi
._keyValues
, 0, i
);
384 if (i
!= _keyValues
.Length
- 1) Array
.Copy(_keyValues
, i
+ 1, multi
._keyValues
, i
, _keyValues
.Length
- i
- 1);
390 // The key does not already exist in this map.
392 if (value == null && treatNullValueAsNonexistent
)
394 // We can simply return this same map, as there's nothing to add or remove.
398 // We need to create a new map that has the additional key/value pair.
399 // If with the addition we can still fit in a multi map, create one.
400 if (_keyValues
.Length
< MaxMultiElements
)
402 var multi
= new MultiElementAsyncLocalValueMap(_keyValues
.Length
+ 1);
403 Array
.Copy(_keyValues
, 0, multi
._keyValues
, 0, _keyValues
.Length
);
404 multi
._keyValues
[_keyValues
.Length
] = new KeyValuePair
<IAsyncLocal
, object?>(key
, value);
408 // Otherwise, upgrade to a many map.
409 var many
= new ManyElementAsyncLocalValueMap(MaxMultiElements
+ 1);
410 foreach (KeyValuePair
<IAsyncLocal
, object?> pair
in _keyValues
)
412 many
[pair
.Key
] = pair
.Value
;
418 public bool TryGetValue(IAsyncLocal key
, out object? value)
420 foreach (KeyValuePair
<IAsyncLocal
, object?> pair
in _keyValues
)
422 if (ReferenceEquals(key
, pair
.Key
))
433 // Instance with any number of key/value pairs.
434 private sealed class ManyElementAsyncLocalValueMap
: Dictionary
<IAsyncLocal
, object?>, IAsyncLocalValueMap
436 public ManyElementAsyncLocalValueMap(int capacity
) : base(capacity
) { }
438 public IAsyncLocalValueMap
Set(IAsyncLocal key
, object? value, bool treatNullValueAsNonexistent
)
441 bool containsKey
= ContainsKey(key
);
443 // If the value being set exists, create a new many map, copy all of the elements from this one,
444 // and then store the new key/value pair into it. This is the most common case.
445 if (value != null || !treatNullValueAsNonexistent
)
447 var map
= new ManyElementAsyncLocalValueMap(count
+ (containsKey
? 0 : 1));
448 foreach (KeyValuePair
<IAsyncLocal
, object?> pair
in this)
450 map
[pair
.Key
] = pair
.Value
;
456 // Otherwise, the value is null and a null value may be treated as nonexistent. We can downgrade to a smaller
457 // map rather than storing null.
459 // If the key is contained in this map, we're going to create a new map that's one pair smaller.
462 // If the new count would be within range of a multi map instead of a many map,
463 // downgrade to the multi map, which uses less memory and is faster to access.
464 // Otherwise, just create a new many map that's missing this key.
465 if (count
== MultiElementAsyncLocalValueMap
.MaxMultiElements
+ 1)
467 var multi
= new MultiElementAsyncLocalValueMap(MultiElementAsyncLocalValueMap
.MaxMultiElements
);
469 foreach (KeyValuePair
<IAsyncLocal
, object?> pair
in this)
471 if (!ReferenceEquals(key
, pair
.Key
))
473 multi
.UnsafeStore(index
++, pair
.Key
, pair
.Value
);
476 Debug
.Assert(index
== MultiElementAsyncLocalValueMap
.MaxMultiElements
);
481 var map
= new ManyElementAsyncLocalValueMap(count
- 1);
482 foreach (KeyValuePair
<IAsyncLocal
, object?> pair
in this)
484 if (!ReferenceEquals(key
, pair
.Key
))
486 map
[pair
.Key
] = pair
.Value
;
489 Debug
.Assert(map
.Count
== count
- 1);
494 // We were storing null and a null value may be treated as nonexistent, but the key wasn't in the map, so
495 // there's nothing to change. Just return this instance.