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