C# 两大线程本地存储解决方案:ThreadStatic 与 ThreadLocal
一、线程本地存储
在 C# 中,static
关键字定义的变量,其作用域是在应用程序域(AppDomain)内共享的。因此,在多线程操作时,对同一个静态变量进行操作可能会导致并发问题,如锁竞争等。这种情况下,我们需要一种机制,使某些变量对每个线程独立,从而避免锁竞争问题。
线程本地存储(Thread-Local Storage,TLS)是一种机制,允许每个线程拥有自己的变量副本。这意味着每个线程对变量的访问都是独立的,互不干扰。
例如,线程安全的 ConcurrentBag
底层的实现就用到了 ThreadLocal
变量,来实现线程间的独立数据存储。
二、C# 中的解决方案
1. ThreadStatic 特性
概念与用法
ThreadStatic
是一个用于静态字段的特性,可以使字段在每个线程中都有一个独立的实例。
示例代码
internal class Program
{[ThreadStatic]private static int _threadSpecificData = -1;static void Main(string[] args){Task.Run(() =>{_threadSpecificData = 1;Console.WriteLine($"第1个线程,线程id: {Environment.CurrentManagedThreadId}: {_threadSpecificData}");});Task.Run(() =>{_threadSpecificData = 2;Console.WriteLine($"第2个线程,线程id:{Environment.CurrentManagedThreadId}: {_threadSpecificData}");});Console.ReadKey();}
}
输出:
第1个线程,线程id: 9: 1
第2个线程,线程id:8: 2
注意事项
只能用于 static
字段。
每个线程使用变量前,初始化变量。定义变量时,赋值的变量初始值,只会有一个线程的变量有这个初始值,其他线程变量副本是没有赋值的状态。特别主要引用类型的变量,会是null.
2. ThreadLocal 类
概念与用法
ThreadLocal<T>
是 .NET 提供的一个泛型类,用于更灵活地实现线程本地存储。它允许每个线程拥有独立的值,并支持通过工厂方法初始化每个线程的值。
示例代码
internal class Program{static void Main(string[] args){ThreadLocal<int> _threadSpecificData = new ThreadLocal<int>(() => Thread.CurrentThread.ManagedThreadId);Task.Run(() =>{Console.WriteLine($"第1个线程,线程id: {Environment.CurrentManagedThreadId}: value: {_threadSpecificData}");});Task.Run(() =>{Console.WriteLine($"第1个线程,线程id: {Environment.CurrentManagedThreadId}: value: {_threadSpecificData}");});Console.ReadKey();}}
输出:
第1个线程,线程id: 8: value: 8
第1个线程,线程id: 6: value: 6
特点
支持 Func 委托延迟初始化
区别
特性 | ThreadStatic | ThreadLocal |
---|---|---|
使用范围 | 仅用于 static 字段 | 不限制 |
初始化方式 | 无法直接初始化,需显式赋值 | 支持延迟初始化 |
管理灵活性 | 简单,但功能受限 | 功能强大,适合复杂场景 |
三、线程本地存储到底存储在什么地方
线程本地存储的引用地址是存在 TLS
(Thread Local Storage)上。
TLS(Thread Local Storage): 用于存储线程私有数据。
TEB(Thread Environment Block): 在线程的本地存储之上,是线程的环境块,保存了线程的上下文信息。
PEB(Process Environment Block): 进程环境块,用于存储进程级的全局信息。
以下是内存结构的示意图:
+----------------+
| PEB |
+----------------+
| TEB | <-- 多个线程
+----------------+
| TLS |
+----------------+
四、应用场景
示例 1:用于日志记录中的线程上下文
在日志记录中,通过线程本地存储可以为每个线程存储独立的上下文信息。在多线程应用程序中,每个线程可能需要记录不同级别的日志。使用ThreadLocal可以为每个线程分配一个独立的日志记录器实例,从而避免日志记录的竞争和同步问题。
internal class Program
{static void Main(string[] args){Task.Run(() =>{logger.SetContext("线程A"); logger.log("在执行第1步操作");Thread.Sleep(200);logger.log("在执行第2步操作");logger.log("执行完了");});Task.Run(() =>{logger.SetContext("线程B");logger.log("在执行第1步操作");logger.log("遇到异常,退出了");});Console.ReadKey();}
}class logger
{private static ThreadLocal<string> _context = new ThreadLocal<string>();public static void SetContext(string context){_context.Value = context;}public static void log(string message){Console.WriteLine($"[{_context.Value}]{message}");}
}
输出:
[线程A]在执行第1步操作
[线程B]在执行第1步操作
[线程B]遇到异常,退出了
[线程A]在执行第2步操作
[线程A]执行完了
五、总结
C# 提供了两种线程本地存储解决方案:
ThreadStatic
:简单直接,但初始化灵活性较差。
ThreadLocal
:功能强大,适合更复杂的场景。
根据具体应用场景选择合适的方案,可以显著提高程序的性能和线程安全性。