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>中添加或删除元素,而这些操作均会影响初始值。

posted @   一纸年华  阅读(568)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
历史上的今天:
2021-09-06 C# 元组
点击右上角即可分享
微信分享提示

目录导航

1.[ThreadStatic]特性
2.ThreadLocal<T>类
ThreadLocal<T>和实例字段
3.GetData方法和SetData方法
4.AsyncLocal<T>类