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
:功能强大,适合更复杂的场景。
根据具体应用场景选择合适的方案,可以显著提高程序的性能和线程安全性。
标签:存储,Console,C#,ThreadLocal,static,线程,logger From: https://www.cnblogs.com/bcsg/p/18664673