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 @ 2022-09-06 11:00  一纸年华  阅读(481)  评论(0编辑  收藏  举报