ThreadStaticAttribute vs ThreadLocal vs AsyncLocal
前言
在.NET程序里,使用static关键字修饰的变量存在于整个程序的生命周期,这使得它可以被多个线程同时访问,这种方式在某些情况下很好用,但是可能需要加锁来处理线程安全问题,在另一些情况下可能只想在线程内共享变量,多个线程之间的变量互相隔离,这就需要使用ThreadStaticAttribute丶ThreadLocal<T>和AsyncLocal<T>
TL;DR
| 实现方式 | 跨线程 | 流向辅助线程 |
|---|---|---|
| static | ✔️ | ✔️ |
| AsyncLocal | ❌ | ✔️ |
| ThreadLocal | ❌ | ❌ |
| ThreadStaticAttribute | ❌ | ❌ |
流向辅助线程
发生线程上下文切换时,能够保存数据并流向到下一个线程,注意,这是单向数据流,辅助线程上更改数据,并不会影响主线程数据(但是你可以通过引用类型来实现主线程数据的同时更改)。
- 线程上下文切换
- new Thread()
- new Task()
- Task.Run()
- ThreadPool.QueueUserWorkItem()
- await
线程隔离
ThreadStaticAttribute和ThreadLocal<T>都实现线程间的隔离,我们先来看ThreadStaticAttribute样例代码:
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>的样例代码:
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所存储的值还继续存在,查看此样例:
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,都无法解决异步编程时的线程数据共享,考虑如下代码:
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>将值传递给线程切换上下文,继续流向下面的线程,样例代码:
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
- ThreadLocal
- AsyncLocal
- 多线程共享变量和 AsyncLocal
- ThreadStatic v.s. ThreadLocal: is generic better than attribute?
- 浅析 .NET 中 AsyncLocal 的实现原理
- Why AsyncLocal is different from CallContext
- AsyncLocal Value is Null after being set from within Application_BeginRequest()
- AsyncLocal loses state when ServiceProviderScope is Disposed