C#线程:本地存储
1.[ThreadStatic]特性
实现线程本地存储最简单的方式是在静态字段上附加ThreadStatic特性:
[ThreadStatic] static int _x;
这样,每个线程都会得到一个_x的独立副本。
但是,[ThreadStatic]并不支持实例字段(它对实例字段并不会产生任何作用);它也不支持和字段初始化器配合使用。因为它们只会在调用静态构造器的线程上执行一次。如果一定要处理实例字段,或者需要使用非默认值,则更推荐使用ThreadLocal<T>
。
2.ThreadLocal<T>类
ThreadLocal<T>
对静态和实例字段都提供了线程本地存储支持,并允许指定默认值。
例如,以下代码为每一个线程创建了一个ThreadLocal<int>
对象,并将其默认值设置为3:
static ThreadLocal<int> _x = new ThreadLocal<int>(()=>3);
此后就可以调用_x的Value属性来访问线程本地值了。ThreadLocal的值是延迟计算的:其中的工厂函数会在(每一个线程)第一次调用时计算实际的值。
ThreadLocal<T>和实例字段
ThreadLocal<T>
也支持实例字段并可以获得局部变量的值。
例如,假设我们需要在一个多线程环境下生成随机数。但Random类不是线程安全的,因此要么在Random对象周围加锁(但是这就会限制并发性),要么为每一个线程生成一个独立的Random对象。而ThreadLocal<T>
可以轻松实现第二种方案:
var localRandom = new ThreadLocal<Random>(() => new Random()); Console.WriteLine(localRandom.Value.Next());
我们在工厂函数中用最简单的方式创建了Random对象。其中,Random的无参数构造器会采用系统时钟作为随机数的种子。但如果两个Random对象是在10毫秒内创建的,则这两个对象就有可能有相同的种子。此时可以使用如下代码修正这个问题:
var localRandom = new ThreadLocal<Random>(() => new Random(Guid.NewGuid().GetHashCode())); Console.WriteLine(localRandom.Value.Next());
3.GetData方法和SetData方法
第三种实现线程本地存储的方式是使用Thread类的GetData和SetData方法。这些方法会将数据存储在线程独有的“插槽”(slot)中。Thread.GetData负责从线程独有的数据存储中读取数据,而Thread.SetData则向其中写入数据。这两个方法都需要使用LocalDataStoreSlot对象来获得这个插槽。所有的线程都可以获得相同的插槽,但是它们的值却是互相独立的。例如:
class Test { // 同一个LocalDataStoreSlot对象可以跨所有线程使用。 LocalDataStoreSlot _secSlot = Thread.GetNamedDataSlot("securityLevel"); // 或用此方法获得匿名插槽 //LocalDataStoreSlot _secSlot = Thread.AllocateDataSlot(); // 这个属性在每个线程上都有一个单独的值。 int SecurityLevel { get { object data = Thread.GetData(_secSlot); return data == null ? 0 : (int)data; } set { Thread.SetData(_secSlot, value); } } }
在这个例子中,我们调用Thread.GetNamedDataSlot来创建一个命名插槽,这样就可以在整个应用程序中共享这个命名插槽了。此外,还可以调用Thread.AllocateDataSlot来获得一个匿名插槽,这样就可以自由控制插槽的使用范围。
Thread.FreeNamedDataSlot方法将释放所有线程中的命名插槽。需要注意的是,只有当LocalDataStoreSlot对象的所有引用都已经在作用域之外并被垃圾回收时插槽才会释放。
4.AsyncLocal<T>类
到目前为止讨论的线程本地存储方案均不适用于异步函数。这是因为await之后的执行可能会恢复到其他线程中。而AsyncLocal<T>
类可以跨越await保存其数据,从而解决上述问题:
static AsyncLocal<string> _asyncLocalTest = new AsyncLocal<string>(); static void Main() { _asyncLocalTest.Value = "test"; await Task.Delay(1000); // 即使返回到另外的线程,以下操作也会执行 Console.WriteLine(_asyncLocalTest.Value); }
AsyncLocal<T>
仍然可以将独立线程间的操作进行隔离(和线程是调用Thread.Start还是Task.Run初始化无关)。以下例子会分别输出“one one”与“two two”:
static AsyncLocal<string> _asyncLocalTest = new AsyncLocal<string>(); static void Main() { // 在两个并发线程上调用两次Test new Thread(() => Test("one")).Start(); new Thread(() => Test("two")).Start(); Console.ReadKey(); } static async void Test(string value) { _asyncLocalTest.Value = value; await Task.Delay(1000); // 即使返回到另外的线程,以下操作也会执行 Console.WriteLine(value + " " + _asyncLocalTest.Value); }
与其他结构相比,AsyncLocal<T>
独特而有趣的点在于:如果AsyncLocal<T>
对象在线程启动时拥有值,则新的线程将“继承”这个值:
static AsyncLocal<string> _asyncLocalTest = new AsyncLocal<string>(); static void Main() { _asyncLocalTest.Value = "test"; new Thread(AnotherMethod).Start(); Console.ReadKey(); } static void AnotherMethod() => Console.WriteLine(_asyncLocalTest.Value); // test
新的线程实际上获得了这个值的一个副本。因此新线程对该值的修改不会影响原始值。
static AsyncLocal<string> _asyncLocalTest = new AsyncLocal<string>(); static void Main() { _asyncLocalTest.Value = "test"; var t = new Thread(AnotherMethod); t.Start(); t.Join(); Console.WriteLine(_asyncLocalTest.Value); // test Console.ReadKey(); } static void AnotherMethod() => _asyncLocalTest.Value = "no-test";
需要注意的是新线程获得的是一个浅表副本,因此如果将AsyncLocal<string>
替换为AsyncLocal<StringBuilder>
或AsyncLocal<List<string>>
,则新线程就可以清空StringBuilder
的内容或者在List<string>
中添加或删除元素,而这些操作均会影响初始值。
本文来自博客园,作者:一纸年华,转载请注明原文链接:https://www.cnblogs.com/nullcodeworld/p/16661055.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
2021-09-06 C# 元组