Preface
For children's shoes that have written about Core, you can useHttpContextAccessorGet the HttpContext outside the Controller, and the key to its implementation is actually a static field of type AsyncLocal<HttpContextHolder>. Next, let’s discuss the specific implementation principles of AsyncLocal with you. If there are any points that are not clear or inaccurate, I hope to point them out.
public class HttpContextAccessor : IHttpContextAccessor { private static AsyncLocal<HttpContextHolder> _httpContextCurrent = new AsyncLocal<HttpContextHolder>(); // Other codes are not displayed here}
The source code of this article is the latest github open source code until the time of publication. It is slightly different from the previous implementation, but the design idea is basically the same.
Codebase address:/dotnet/runtime
1. Thread local storage
If you want to share a variable in the entire .NET program, you can place the variable you want to share in a certain classStatic propertiesCome up and realize it.
In a multi-threaded running environment, it may be desirable to narrow the shared scope of this variable to a single thread. For example, in a web application, the server allocates an independent thread for each simultaneous access request. When we want to maintain the information of our current access user in these independent threads, we need to store the thread locally.
For example, the following example.
class Program { [ThreadStatic] private static string _value; static void Main(string[] args) { (0, 4, _ => { var threadId = ; _value ??= $"This is from the thread{threadId}Data of"; ($"Thread:{threadId}; Value:{_value}"); }); } }
Output result:
Thread: 4; Value: This is the data from thread 4
Thread: 1; Value: This is the data from thread 1
Thread: 5; Value: This is the data from thread 5
Thread: 6; Value: This is the data from thread 6
In addition to being able to useThreadStaticAttributeIn addition, we can also useThreadLocal<T> 、CallContext 、AsyncLocal<T>to achieve the same function. because.NET CoreNo more implementationCallContext, so the following code can only be found in.NET FrameworkExecute in .
class Program { [ThreadStatic] private static string _threadStatic; private static ThreadLocal<string> _threadLocal = new ThreadLocal<string>(); private static AsyncLocal<string> _asyncLocal = new AsyncLocal<string>(); static void Main(string[] args) { (0, 4, _ => { var threadId = ; var value = $"This is from the thread{threadId}Data of"; _threadStatic ??= value; ("value", value); _threadLocal.Value ??= value; _asyncLocal.Value ??= value; ($"Use ThreadStaticAttribute; Thread:{threadId}; Value:{_threadStatic}"); ($"Use CallContext; Thread:{threadId}; Value:{("value")}"); ($"Use ThreadLocal; Thread:{threadId}; Value:{_threadLocal.Value}"); ($"Use AsyncLocal; Thread:{threadId}; Value:{_asyncLocal.Value}"); }); (); } }
Output result:
Use ThreadStaticAttribute; Thread: 3; Value: This is the data from thread 3
Use ThreadStaticAttribute; Thread: 4; Value: This is the data from thread 4
Use ThreadStaticAttribute; Thread: 1; Value: This is the data from thread 1
Use CallContext; Thread: 1; Value: This is the data from thread 1
Use ThreadLocal; Thread: 1; Value: This is the data from thread 1
Use AsyncLocal; Thread: 1; Value: This is the data from thread 1
Use ThreadStaticAttribute; Thread: 5; Value: This is the data from thread 5
Use CallContext; Thread: 5; Value: This is the data from thread 5
Use ThreadLocal; Thread: 5; Value: This is the data from thread 5
Use AsyncLocal; Thread: 5; Value: This is the data from thread 5
Use CallContext; Thread: 3; Value: This is the data from thread 3
Use CallContext; Thread: 4; Value: This is the data from thread 4
Use ThreadLocal; Thread: 4; Value: This is the data from thread 4
Use AsyncLocal; Thread: 4; Value: This is the data from thread 4
Use ThreadLocal; Thread: 3; Value: This is the data from thread 3
Use AsyncLocal; Thread: 3; Value: This is the data from thread 3
The above examples are just to store and retrieve threads in the same thread, but in the daily development process, we will have many asynchronous scenarios, which may cause the threads executing the code to switch.
For example, the following example
class Program { [ThreadStatic] private static string _threadStatic; private static ThreadLocal<string> _threadLocal = new ThreadLocal<string>(); private static AsyncLocal<string> _asyncLocal = new AsyncLocal<string>(); static void Main(string[] args) { _threadStatic = "ThreadStatic saved data"; _threadLocal.Value = "ThreadLocal saved data"; _asyncLocal.Value = "AsyncLocal saved data"; PrintValuesInAnotherThread(); (); } private static void PrintValuesInAnotherThread() { (() => { ($"ThreadStatic: {_threadStatic}"); ($"ThreadLocal: {_threadLocal.Value}"); ($"AsyncLocal: {_asyncLocal.Value}"); }); } }
Output result:
ThreadStatic:
ThreadLocal:
AsyncLocal: Data saved by AsyncLocal
After the thread switches, only AsyncLocal can retain the original value. Of course, CallContext in the .NET Framework can also implement this requirement. Here is a relatively complete summary.
Implementation method | .NET FrameWork is available | .NET Core is available | Whether data flow to auxiliary threads is supported |
---|---|---|---|
ThreadStaticAttribute | yes | yes | no |
ThreadLocal<T> | yes | yes | no |
(string name, object data) | yes | no | Supports only when the type corresponding to the parameter data implements the ILogicalThreadAffinative interface |
(string name, object data) | yes | no | yes |
AsyncLocal<T> | yes | yes | yes |
2. AsyncLocal implementation
We mainly learn the source code of .NET Core, the source code address:/dotnet/runtime/blob/master/src/libraries//src/System/Threading/
2.1. Subject AsyncLocal<T>
AsyncLocal<T> provides us with two functions
- Access values through the Value attribute
- Register the callback function through the constructor to listen for changes to values in any thread. You need to remember this function. There will be many things to do when introducing the source code later.
Its internal code is relatively simple
public sealed class AsyncLocal<T> : IAsyncLocal { private readonly Action<AsyncLocalValueChangedArgs<T>>? m_valueChangedHandler; // Non-parameter structure public AsyncLocal() { } // You can register the constructor of the callback. When Value is changed in any thread, the callback will be called public AsyncLocal(Action<AsyncLocalValueChangedArgs<T>>? valueChangedHandler) { m_valueChangedHandler = valueChangedHandler; } [MaybeNull] public T Value { get { // Get the value from the ExecutionContext as the Key object? obj = (this); return (obj == null) ? default : (T)obj; } // Whether to register a callback will affect whether the ExecutionContext saves its reference set => (this, value, m_valueChangedHandler != null); } // In ExecutionContext, if the value has changed, this method will be called void (object? previousValueObj, object? currentValueObj, bool contextChanged) { (m_valueChangedHandler != null); T previousValue = previousValueObj == null ? default! : (T)previousValueObj; T currentValue = currentValueObj == null ? default! : (T)currentValueObj; m_valueChangedHandler(new AsyncLocalValueChangedArgs<T>(previousValue, currentValue, contextChanged)); } } internal interface IAsyncLocal { void OnValueChanged(object? previousValue, object? currentValue, bool contextChanged); }
The real data access is throughand
Implemented.
public class ExecutionContext { internal static object? GetLocalValue(IAsyncLocal local); internal static void SetLocalValue( IAsyncLocal local, object? newValue, bool needChangeNotifications); }
It should be noted that this is passedIAsyncLocalThis interface has been implementedAsyncLocalandExcutionContextdecoupling.ExcutionContextOnly pay attention to the data access itself, the types defined by the interface are all object, not the specific types.T。
2.2. Data access implementation of AsyncLocal<T> in ExecutionContext
In .NET, each thread is associated with aExecution context(execution context) . Can be passedAttributes are accessed, or through()Get (the implementation of the former).
AsyncLocalIn the end, it is to save the data inExecutionContextTo understand more deeplyAsyncLocalWe need to understand it first.
Source code address:/dotnet/runtime/blob/master/src/libraries//src/System/Threading/
2.2.1. The binding relationship between ExecutionContext and thread
ExecutionContext is saved on the internal modified _executionContext field of Thread. butnot directly exposed _executionContext but with()Share a set of logic.
class ExecutionContext { public static ExecutionContext? Capture() { ExecutionContext? executionContext = ._executionContext; if (executionContext == null) { executionContext = Default; } else if (executionContext.m_isFlowSuppressed) { executionContext = null; } return executionContext; } }
Below is the finished part related to ExecutionContext of Thread. Thread belongs to a partial class. The _executionContext field is defined inIn the file
class Thread { // Save the execution context associated with the current thread internal ExecutionContext? _executionContext; [ThreadStatic] private static Thread? t_currentThread; public static Thread CurrentThread => t_currentThread ?? InitializeCurrentThread(); public ExecutionContext? ExecutionContext => (); }
2.2.2. Private variables of ExecutionContext
public sealed class ExecutionContext : IDisposable, ISerializable { // Default execution context internal static readonly ExecutionContext Default = new ExecutionContext(isDefault: true); // Default context after execution context prohibits flow internal static readonly ExecutionContext DefaultFlowSuppressed = new ExecutionContext(, <IAsyncLocal>(), isFlowSuppressed: true); // Save the Value values of all AsyncLocals registered with modified callbacks. This article does not involve specific discussions on this field. private readonly IAsyncLocalValueMap? m_localValues; // Save all AsyncLocal object references registered with callbacks private readonly IAsyncLocal[]? m_localChangeNotifications; // Whether the current thread prohibits context flow private readonly bool m_isFlowSuppressed; // Is the current context the default context private readonly bool m_isDefault; }
2.2.3. IAsyncLocalValueMap interface and its implementation
In the same thread, allAsyncLocalSavedValueAll saved inExecutionContextofm_localValueson the field.
public class ExecutionContext { private readonly IAsyncLocalValueMap m_localValues; }
To optimize performance when looking for values, Microsoft provides 6 implementations for IAsyncLocalValueMap
type | Number of elements |
---|---|
EmptyAsyncLocalValueMap | 0 |
OneElementAsyncLocalValueMap | 1 |
TwoElementAsyncLocalValueMap | 2 |
ThreeElementAsyncLocalValueMap | 3 |
MultiElementAsyncLocalValueMap | 4 ~ 16 |
ManyElementAsyncLocalValueMap | > 16 |
As the number of AsyncLocals associated with ExecutionContext increases, the implementation of IAsyncLocalValueMap will be in the SetLocalValue method of ExecutionContextContinuous replacement. QueryedTime complexity and space complexity increase in sequence. The code implementation belongs to the same file as AsyncLocal. Of course, when the number of elements is reduced, it will also be replaced with the previous implementation.
// This interface is used to save the mapping relationship of IAsyncLocal => object in the ExecutionContext.// Its implementation is set to immutable, and as the number of elements increases, the spatial complexity and time complexity also increase.internal interface IAsyncLocalValueMap { bool TryGetValue(IAsyncLocal key, out object? value); // Add AsyncLocal or modify existing AsyncLocal through this method // If the number does not change, return the IAsyncLocalValueMap implementation class instance of the same type. // If the number changes (increase or decrease, it will decrease when the value is set to null), it may return different types of IAsyncLocalValueMap implementation class instances IAsyncLocalValueMap Set(IAsyncLocal key, object? value, bool treatNullValueAsNonexistent); }
Map is created with a static classAsyncLocalValueMapThe Create method serves as the entry to create.
internal static class AsyncLocalValueMap { // EmptyAsyncLocalValueMap is only instantiated here, and is used as a constant in other places. public static IAsyncLocalValueMap Empty { get; } = new EmptyAsyncLocalValueMap(); public static bool IsEmpty(IAsyncLocalValueMap asyncLocalValueMap) { (asyncLocalValueMap != null); (asyncLocalValueMap == Empty || () != typeof(EmptyAsyncLocalValueMap)); return asyncLocalValueMap == Empty; } public static IAsyncLocalValueMap Create(IAsyncLocal key, object? value, bool treatNullValueAsNonexistent) { // Create the initial instance // If AsyncLocal registers a callback, it is necessary to save the value of null so that the callback will be triggered due to the change in the next time a non-null value is set return value != null || !treatNullValueAsNonexistent ? new OneElementAsyncLocalValueMap(key, value) : Empty; } }
After that, every time the element is updated, the Set method of the IAsyncLocalValueMap implementation class must be called. The original instance will not change and the return value of Set needs to be saved.
Next,ThreeElementAsyncLocalValueMapExplain as an example
private sealed class ThreeElementAsyncLocalValueMap : IAsyncLocalValueMap { // Declare three private fields to save key private readonly IAsyncLocal _key1, _key2, _key3; // Declare three private fields to save private readonly object? _value1, _value2, _value3; public ThreeElementAsyncLocalValueMap(IAsyncLocal key1, object? value1, IAsyncLocal key2, object? value2, IAsyncLocal key3, object? value3) { _key1 = key1; _value1 = value1; _key2 = key2; _value2 = value2; _key3 = key3; _value3 = value3; } public IAsyncLocalValueMap Set(IAsyncLocal key, object? value, bool treatNullValueAsNonexistent) { // If AsyncLocal has registered a callback, the value of treatNullValueAsNoneexistent is false. // means that even if value is null, it is considered valid if (value != null || !treatNullValueAsNonexistent) { // If the current map has saved the incoming key, a new map instance with updated value is returned if (ReferenceEquals(key, _key1)) return new ThreeElementAsyncLocalValueMap(key, value, _key2, _value2, _key3, _value3); if (ReferenceEquals(key, _key2)) return new ThreeElementAsyncLocalValueMap(_key1, _value1, key, value, _key3, _value3); if (ReferenceEquals(key, _key3)) return new ThreeElementAsyncLocalValueMap(_key1, _value1, _key2, _value2, key, value); // If the current key does not exist in the map, a map that can store the fourth key is needed var multi = new MultiElementAsyncLocalValueMap(4); (0, _key1, _value1); (1, _key2, _value2); (2, _key3, _value3); (3, key, value); return multi; } else { // value is null, the corresponding key will be ignored or removed from the map. There will be two situations here // 1. If the current key exists in the map, remove this key and the map type is downgraded to TwoElementAsyncLocalValueMap return ReferenceEquals(key, _key1) ? new TwoElementAsyncLocalValueMap(_key2, _value2, _key3, _value3) : ReferenceEquals(key, _key2) ? new TwoElementAsyncLocalValueMap(_key1, _value1, _key3, _value3) : ReferenceEquals(key, _key3) ? new TwoElementAsyncLocalValueMap(_key1, _value1, _key2, _value2) : // 2. If the current key does not exist in the map, it will be directly ignored (IAsyncLocalValueMap)this; } } // You can find the corresponding value by comparing it at most three times public bool TryGetValue(IAsyncLocal key, out object? value) { if (ReferenceEquals(key, _key1)) { value = _value1; return true; } else if (ReferenceEquals(key, _key2)) { value = _value2; return true; } else if (ReferenceEquals(key, _key3)) { value = _value3; return true; } else { value = null; return false; } } }
2.2.4、ExecutionContext - SetLocalValue
It should be noted that there will be two involved hereImmutableStructure, one isExecutionContextOriginal, the other one isIAsyncLocalValueMapImplementation class. After the value changes before and after the same key, a new instance of ExecutionContext and an IAsyncLocalMap implementation class instance will be generated (inIAsyncLocalValueMapImplementation classSetCompleted in the method).
internal static void SetLocalValue(IAsyncLocal local, object? newValue, bool needChangeNotifications) { // Get the current execution context ExecutionContext? current = ._executionContext; object? previousValue = null; bool hadPreviousValue = false; if (current != null) { (!); (current.m_localValues != null, "Only the default context should have null, and we shouldn't be here on the default context"); // Determine whether the AsyncLocal currently has a corresponding Value as a Key hadPreviousValue = current.m_localValues.TryGetValue(local, out previousValue); } // If the value has not changed twice, continue processing if (previousValue == newValue) { return; } // Description of treatmentNullValueAsNoneexistent: !needChangeNotifications // If AsyncLocal registers a callback, then needChangeNotifications is ture, m_localValues will save the null value so that the change callback will be triggered next time IAsyncLocal[]? newChangeNotifications = null; IAsyncLocalValueMap newValues; bool isFlowSuppressed = false; if (current != null) { (!); (current.m_localValues != null, "Only the default context should have null, and we shouldn't be here on the default context"); isFlowSuppressed = current.m_isFlowSuppressed; // This step is very critical. Modify the map by calling m_localValues.Set, which will produce a new map instance. newValues = current.m_localValues.Set(local, newValue, treatNullValueAsNonexistent: !needChangeNotifications); newChangeNotifications = current.m_localChangeNotifications; } else { // If the current context does not exist, create the first IAsyncLocalValueMap instance newValues = (local, newValue, treatNullValueAsNonexistent: !needChangeNotifications); } // If AsyncLocal registers a callback, you need to save the reference to AsyncLocal // There will be two situations here, one is that the array has not been created, and the other is that the array has existed if (needChangeNotifications) { if (hadPreviousValue) { (newChangeNotifications != null); ((newChangeNotifications, local) >= 0); } else if (newChangeNotifications == null) { newChangeNotifications = new IAsyncLocal[1] { local }; } else { int newNotificationIndex = ; // This method will create a new array and copy the original element in the past (ref newChangeNotifications, newNotificationIndex + 1); newChangeNotifications[newNotificationIndex] = local; } } // If AsyncLocal has a valid value and allows execution context flow, a new ExecutionContext instance is created, and the new instance will save all AsyncLocal values and all AsyncLocal references that need to be notified. ._executionContext = (!isFlowSuppressed && (newValues)) ? null : // No values, return to Default context new ExecutionContext(newValues, newChangeNotifications, isFlowSuppressed); if (needChangeNotifications) { // Call the previously registered delegation (previousValue, newValue, contextChanged: false); } }
2.2.5、ExecutionContext - GetLocalValue
The acquisition of values is relatively simple
internal static object? GetLocalValue(IAsyncLocal local) { ExecutionContext? current = ._executionContext; if (current == null) { return null; } (!); (current.m_localValues != null, "Only the default context should have null, and we shouldn't be here on the default context"); current.m_localValues.TryGetValue(local, out object? value); return value; }
3. ExecutionContext flow
When a thread switch occurs,ExecutionContextIt will be captured by default in the previous thread and flow to the next thread, and the data it saves will flow with it.
In all places where thread switching occurs, the basic class library (BCL) encapsulates capture of the execution context for us.
For example:
- new Thread(ThreadStart start).Start()
- (Action action)
- (WaitCallback callBack)
- await syntax sugar
class Program { static AsyncLocal<string> _asyncLocal = new AsyncLocal<string>(); static async Task Main(string[] args) { _asyncLocal.Value = "AsyncLocal saved data"; new Thread(() => { ($"new Thread: {_asyncLocal.Value}"); }) { IsBackground = true }.Start(); (_ => { ($": {_asyncLocal.Value}"); }); (() => { ($": {_asyncLocal.Value}"); }); await (100); ($"after await: {_asyncLocal.Value}"); } }
Output result:
new Thread: Data saved by AsyncLocal
: Data saved by AsyncLocal
: Data saved by AsyncLocal
After await: Data saved by AsyncLocal
3.1. Prohibition and recovery of flow
ExecutionContextProvide us withSuppressFlow(Flow is prohibited) andRestoreFlow(Restoring flow) These two static methods control whether the execution context of the current thread flows like a helper thread. And can be passedIsFlowSuppressedStatic method to make judgments.
class Program { static AsyncLocal<string> _asyncLocal = new AsyncLocal<string>(); static async Task Main(string[] args) { _asyncLocal.Value = "AsyncLocal saved data"; ("default:"); PrintAsync(); // Without await, the subsequent thread will not be switched (1000); // Make sure that all threads in the above method are executed (); ("SuppressFlow:"); PrintAsync(); (1000); ("RestoreFlow:"); (); await PrintAsync(); (); } static async ValueTask PrintAsync() { new Thread(() => { ($" new Thread: {_asyncLocal.Value}"); }) { IsBackground = true }.Start(); (100); // Ensure the output order (_ => { ($" : {_asyncLocal.Value}"); }); (100); (() => { ($" : {_asyncLocal.Value}"); }); await (100); ($" after await: {_asyncLocal.Value}"); (); } }
Output result:
default:
new Thread: Data saved by AsyncLocal
: Data saved by AsyncLocal
: Data saved by AsyncLocal
After await: Data saved by AsyncLocal
SuppressFlow:
new Thread:
:
:
after await:
RestoreFlow:
new Thread: Data saved by AsyncLocal
: Data saved by AsyncLocal
: Data saved by AsyncLocal
After await: Data saved by AsyncLocal
It should be noted that thread B is called before it is created in thread AIt will only affectExecutionContextPassing from thread A => thread B, thread B => thread C is not affected.
class Program { static AsyncLocal<string> _asyncLocal = new AsyncLocal<string>(); static void Main(string[] args) { _asyncLocal.Value = "A => B"; (); new Thread((() => { ($"ThreadB:{_asyncLocal.Value}"); // Output thread B: _asyncLocal.Value = "B => C"; new Thread((() => { ($"ThreadC:{_asyncLocal.Value}"); // Output thread C: B => C })) { IsBackground = true }.Start(); })) { IsBackground = true }.Start(); (); } }
3.2. Flow implementation of ExcutionContext
The above examples are four scenarios. Since the delivery process of each scenario is relatively complicated, we will introduce one of them first.
But no matter what scenario, the Run method of ExcutionContext will be involved. The RunInternal method is called in the Run method.
public static void Run(ExecutionContext executionContext, ContextCallback callback, object? state) { if (executionContext == null) { ThrowNullContext(); } // The RestoreChangedContextToThread method will be called internally RunInternal(executionContext, callback, state); }
RunInternal calls the following RestoreChangedContextToThread method to assign the ExcutionContext passed to the _executionContext field of the current thread.
internal static void RestoreChangedContextToThread(Thread currentThread, ExecutionContext? contextToRestore, ExecutionContext? currentContext) { (currentThread == ); (contextToRestore != currentContext); // Assign the previous ExecutionContext to the current thread here currentThread._executionContext = contextToRestore; if ((currentContext != null && ) || (contextToRestore != null && )) { OnValuesChanged(currentContext, contextToRestore); } }
3.2.1. new Thread(ThreadStart start).Start() is an example to illustrate the flow of ExecutionContext
This can be divided into three steps:
Capture the current ExecutionContext in the Start method of Thread and pass it to the instantiated ThreadHelper instance in the Thread constructor. The ExecutionContext will temporarily exist in the instance field of ThreadHelper. After the thread is created, it will be called to assign it to the newly created thread.
Code location:
/dotnet/runtime/blob/5fca04171171f118bca0f93aa9741f205b8cdc29/src/coreclr/src//src/System/Threading/#L200
public void Start() { #if FEATURE_COMINTEROP_APARTMENT_SUPPORT // Eagerly initialize the COM Apartment state of the thread if we're allowed to. StartupSetApartmentStateInternal(); #endif // FEATURE_COMINTEROP_APARTMENT_SUPPORT // Attach current thread's security principal object to the new // thread. Be careful not to bind the current thread to a principal // if it's not already bound. if (_delegate != null) { // If we reach here with a null delegate, something is broken. But we'll let the StartInternal method take care of // reporting an error. Just make sure we don't try to dereference a null delegate. (_delegate.Target is ThreadHelper); // Since _delegate points to the instance method of ThreadHelper, _delegate.Target points to the ThreadHelper instance. var t = (ThreadHelper)_delegate.Target; ExecutionContext? ec = (); (ec); } StartInternal(); }
/dotnet/runtime/blob/5fca04171171f118bca0f93aa9741f205b8cdc29/src/coreclr/src//src/System/Threading/#L26
class ThreadHelper { internal ThreadHelper(Delegate start) { _start = start; } internal void SetExecutionContextHelper(ExecutionContext? ec) { _executionContext = ec; } // This method is to wrap the delegate passed in by the Thread constructor internal void ThreadStart() { (_start is ThreadStart); ExecutionContext? context = _executionContext; if (context != null) { // Bind the ExecutionContext with CurrentThread (context, s_threadStartContextCallback, this); } else { InitializeCulture(); ((ThreadStart)_start)(); } } }
4. Summary
AsyncLocal itself does not save data, the data is saved inExecutionContextExamplem_localValuesOn the private field, the field type definition isIAsyncLocalMap,byIAsyncLocal => objectThe Map structure of is saved, and the implementation type changes with the number of elements.
ExecutionContextExample Save in._executionContextOn the top, realize the association with the current thread.
For IAsyncLocalMap implementation classes, if AsyncLocal registers a callback, the value pass null will not be ignored.
There are two situations when the callback is not registered:If the key exists, then delete the map type may be downgraded.If the key does not exist, it will be ignored directly.
The implementation classes of ExecutionContext and IAsyncLocalMap are designed toImmutable. After the value changes before and after the same key, a new instance of the ExecutionContext and an IAsyncLocalMap implementation class instance will be generated.
ExecutionContext is bound to the current thread,By default flow to the secondary thread, flow can be prohibited and restored, and the prohibited flow can only affect the delivery of the current thread to its auxiliary thread, and does not affect the subsequent.
This is the end of this article about a brief analysis of the implementation principle of AsyncLocal in .NET. For more related .NET AsyncLocal content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!