Loading... ## 前言 在`.NET`程序里,使用`static`关键字修饰的变量存在于整个程序的生命周期,这使得它可以被多个线程同时访问,这种方式在某些情况下很好用,但是可能需要加锁来处理线程安全问题,在另一些情况下可能只想在线程内共享变量,多个线程之间的变量互相隔离,这就需要使用`ThreadStaticAttribute`丶`ThreadLocal<T>`和`AsyncLocal<T>` ## TL;DR | 实现方式 | 跨线程 | 流向辅助线程 | :---: | :-------: | :---------: | | static | ✔️ | ✔️ | | AsyncLocal<T> | ❌ | ✔️ | | ThreadLocal<T> | ❌ | ❌ | | ThreadStaticAttribute | ❌ | ❌ | #### 流向辅助线程 发生线程上下文切换时,能够保存数据并流向到下一个线程,注意,这是单向数据流,辅助线程上更改数据,并不会影响主线程数据(但是你可以通过引用类型来实现主线程数据的同时更改)。 * 线程上下文切换 * new Thread() * new Task() * Task.Run() * ThreadPool.QueueUserWorkItem() * await ## 线程隔离 `ThreadStaticAttribute`和`ThreadLocal<T>`都实现线程间的隔离,我们先来看`ThreadStaticAttribute`样例代码: ```csharp class Program { [ThreadStatic] internal static int _counter = 1; static void Main(string[] args) { Console.WriteLine($"Main Thread counter: {_counter}"); Thread thread = new Thread(() => { Console.WriteLine($"Thread 1 counter: {_counter}"); _counter += 10; Console.WriteLine($"Thread 1 counter: {_counter}"); }); thread.Start(); //等待线程结束 thread.Join(); Console.WriteLine($"Main Thread counter: {_counter}"); } } // 输出: // Main Thread counter: 1 // Thread 1 counter: 0 // Thread 1 counter: 10 // Main Thread counter: 1 ``` `_counter`是我们单个线程共享的变量,并且我们给它初始化为`1`。从输出结果可以看出,每个线程的计数都是独立进行的,但是第二个线程的`_counter`没有被初始化,输出的值为默认值`0`。这是由于`ThreadStaticAttribute`标记的字段的初始值只初始化一次,因此只影响一个线程,其余线程则为默认值。 接下来看看`ThreadLocal<T>`的样例代码: ```csharp class Program { internal static ThreadLocal<int> _counter = new ThreadLocal<int>(() => 1); static void Main(string[] args) { Console.WriteLine($"Main Thread counter: {_counter.Value}"); Thread thread = new Thread(() => { Console.WriteLine($"Thread 1 counter: {_counter.Value}"); _counter.Value += 10; Console.WriteLine($"Thread 1 counter: {_counter.Value}"); }); thread.Start(); //等待线程结束 thread.Join(); Console.WriteLine($"Main Thread counter: {_counter.Value}"); } } // 输出: // Main Thread counter: 1 // Thread 1 counter: 1 // Thread 1 counter: 11 // Main Thread counter: 1 ``` 与`ThreadStaticAttribute`的输出结果稍有差别,每个线程的计数还是独立进行,但是初始化值能够被所有线程正确的初始化。 使用`ThreadLocal<T>`与`ThreadStaticAttribute`要注意在使用线程池线程时,线程回到线程池不会被销毁,下次使用时先前`ThreadLocal<T>`与`ThreadStaticAttribute`所存储的值还继续存在,查看此样例: ```csharp class Program { internal static ThreadLocal<int> _counter = new ThreadLocal<int>(() => 1); static void Main(string[] args) { Console.WriteLine($"Main Thread counter: {_counter.Value}"); for (var i = 0; i < 10; i++) { Task.Run(() => { _counter.Value += 10; Console.WriteLine($"Thread{Thread.CurrentThread.ManagedThreadId} counter+=: {_counter.Value}"); }); } Console.ReadLine(); } } // 输出: // Main Thread counter: 1 // Thread5 counter+=: 11 // Thread4 counter+=: 11 // Thread7 counter+=: 11 // Thread8 counter+=: 11 // Thread9 counter+=: 11 // Thread10 counter+=: 11 // Thread11 counter+=: 11 // Thread6 counter+=: 11 // Thread7 counter+=: 21 // Thread10 counter+=: 21 ``` 可以看出,当`Thread7/Thread10`回到线程池后,它们所存储的`_counter`值并没有被清除,下次继续使用`Thread7/Thread10`两个线程时,会累加上记录的值。 综上所述,`ThreadLocal<T>`似乎可以完全替代`ThreadStaticAttribute`进行使用,当然,如果你使用的是`.NET Framework 4.0`以下的版本,则只能使用`ThreadStaticAttribute`。 ## 异步编程的线程间数据共享 无论是`ThreadLocal<T>`还是`ThreadStaticAttribute`,都无法解决异步编程时的线程数据共享,考虑如下代码: ```csharp class Program { internal static ThreadLocal<int> _counter = new ThreadLocal<int>(); static async Task Main(string[] args) { await AsyncMethodA(); } static async Task AsyncMethodA() { _counter.Value = 10; Console.WriteLine($"Thread{Thread.CurrentThread.ManagedThreadId} Main Thread counter: {_counter.Value}"); await AsyncMethodB(); } static async Task AsyncMethodB() { await Task.Delay(1000); Console.WriteLine($"Thread{Thread.CurrentThread.ManagedThreadId} After Delay counter: {_counter.Value}"); } } // 输出: // Thread1 Main Thread counter: 10 // Thread4 After Delay counter: 0 ``` 通过输出可以看出,因为异步编程会导致线程上下文切换,所以`ThreadLocal<T>`无法在异步中保证数据共享。 解决此问题我们可以使用`AsyncLocal<T>`将值传递给线程切换上下文,继续流向下面的线程,样例代码: ```csharp class Program { internal static AsyncLocal<int> _counter = new AsyncLocal<int>(); static async Task Main(string[] args) { await AsyncMethodA(); await AsyncMethodC(); } static async Task AsyncMethodA() { _counter.Value = 10; Console.WriteLine($"Thread{Thread.CurrentThread.ManagedThreadId} Main Thread counter: {_counter.Value}"); await AsyncMethodB(); } static async Task AsyncMethodB() { await Task.Delay(1000); Console.WriteLine($"Thread{Thread.CurrentThread.ManagedThreadId} After Delay counter: {_counter.Value}"); } static async Task AsyncMethodC() { await Task.Delay(1000); Console.WriteLine($"Thread{Thread.CurrentThread.ManagedThreadId} AsyncMethodC counter: {_counter.Value}"); } } // 输出: // Thread1 Main Thread counter: 10 // Thread4 After Delay counter: 10 // Thread4 AsyncMethodC counter: 0 ``` 从输出可以看到,`Thread1`发生线程切换时,将`_counter`值流向了下面的线程既`Thread4`。 ## Reference * [ThreadStaticAttribute](https://docs.microsoft.com/en-us/dotnet/api/system.threadstaticattribute?view=netcore-3.1) * [ThreadLocal<T>](https://docs.microsoft.com/en-us/dotnet/api/system.threading.threadlocal-1?view=netcore-3.1) * [AsyncLocal<T>](https://docs.microsoft.com/en-us/dotnet/api/system.threading.asynclocal-1?view=netframework-4.8) * [多线程共享变量和 AsyncLocal](https://www.cnblogs.com/lonelyxmas/p/12317181.html) * [ThreadStatic v.s. ThreadLocal<T>: is generic better than attribute?](https://stackoverflow.com/questions/18333885/threadstatic-v-s-threadlocalt-is-generic-better-than-attribute) * [浅析 .NET 中 AsyncLocal 的实现原理](https://www.cnblogs.com/blurhkh/p/12240767.html) * [Why AsyncLocal is different from CallContext](https://stackoverflow.com/questions/41825375/why-asynclocal-is-different-from-callcontext) * [AsyncLocal Value is Null after being set from within Application_BeginRequest()](https://stackoverflow.com/questions/43391498/asynclocal-value-is-null-after-being-set-from-within-application-beginrequest) * [AsyncLocal loses state when ServiceProviderScope is Disposed](https://github.com/dotnet/aspnetcore/issues/3249) 最后修改:2021 年 09 月 06 日 © 允许规范转载 赞 0 如果觉得我的文章对你有用,请随意赞赏