ThreadStaticAttribute vs ThreadLocal vs AsyncLocal

分享

前言

.NET程序里,使用static关键字修饰的变量存在于整个程序的生命周期,这使得它可以被多个线程同时访问,这种方式在某些情况下很好用,但是可能需要加锁来处理线程安全问题,在另一些情况下可能只想在线程内共享变量,多个线程之间的变量互相隔离,这就需要使用ThreadStaticAttributeThreadLocal<T>AsyncLocal<T>

TL;DR

实现方式 跨线程 流向辅助线程
static ✔️ ✔️
AsyncLocal ✔️
ThreadLocal
ThreadStaticAttribute

流向辅助线程

发生线程上下文切换时,能够保存数据并流向到下一个线程,注意,这是单向数据流,辅助线程上更改数据,并不会影响主线程数据(但是你可以通过引用类型来实现主线程数据的同时更改)。

  • 线程上下文切换
    • new Thread()
    • new Task()
    • Task.Run()
    • ThreadPool.QueueUserWorkItem()
    • await

线程隔离

ThreadStaticAttributeThreadLocal<T>都实现线程间的隔离,我们先来看ThreadStaticAttribute样例代码:

class Program
{
    [ThreadStatic]
    internal static int _counter = 1;
    static void Main(string[] args)
    {
        Console.WriteLine($"Main Thread counter: {_counter}");
        Thread thread = new Thread(() =>
        {
            Console.WriteLine($"Thread 1 counter: {_counter}");
            _counter += 10;
            Console.WriteLine($"Thread 1 counter: {_counter}");
        });
        thread.Start();
        //等待线程结束
        thread.Join();
        Console.WriteLine($"Main Thread counter: {_counter}");
    }
}
// 输出:
// Main Thread counter: 1
// Thread 1 counter: 0
// Thread 1 counter: 10
// Main Thread counter: 1

_counter是我们单个线程共享的变量,并且我们给它初始化为1。从输出结果可以看出,每个线程的计数都是独立进行的,但是第二个线程的_counter没有被初始化,输出的值为默认值0。这是由于ThreadStaticAttribute标记的字段的初始值只初始化一次,因此只影响一个线程,其余线程则为默认值。

接下来看看ThreadLocal<T>的样例代码:

class Program
{
    internal static ThreadLocal<int> _counter = new ThreadLocal<int>(() => 1);
    static void Main(string[] args)
    {
        Console.WriteLine($"Main Thread counter: {_counter.Value}");
        Thread thread = new Thread(() =>
        {
            Console.WriteLine($"Thread 1 counter: {_counter.Value}");
            _counter.Value += 10;
            Console.WriteLine($"Thread 1 counter: {_counter.Value}");
        });
        thread.Start();
        //等待线程结束
        thread.Join();
        Console.WriteLine($"Main Thread counter: {_counter.Value}");
    }
}
// 输出:
// Main Thread counter: 1
// Thread 1 counter: 1
// Thread 1 counter: 11
// Main Thread counter: 1

ThreadStaticAttribute的输出结果稍有差别,每个线程的计数还是独立进行,但是初始化值能够被所有线程正确的初始化。
使用ThreadLocal<T>ThreadStaticAttribute要注意在使用线程池线程时,线程回到线程池不会被销毁,下次使用时先前ThreadLocal<T>ThreadStaticAttribute所存储的值还继续存在,查看此样例:

class Program
{
    internal static ThreadLocal<int> _counter = new ThreadLocal<int>(() => 1);
    static void Main(string[] args)
    {
        Console.WriteLine($"Main Thread counter: {_counter.Value}");
        for (var i = 0; i < 10; i++)
        {
            Task.Run(() =>
            {
                _counter.Value += 10;
                Console.WriteLine($"Thread{Thread.CurrentThread.ManagedThreadId} counter+=: {_counter.Value}");
            });
        }

        Console.ReadLine();
    }
}
// 输出:
// Main Thread counter: 1
// Thread5 counter+=: 11
// Thread4 counter+=: 11
// Thread7 counter+=: 11
// Thread8 counter+=: 11
// Thread9 counter+=: 11
// Thread10 counter+=: 11
// Thread11 counter+=: 11
// Thread6 counter+=: 11
// Thread7 counter+=: 21
// Thread10 counter+=: 21

可以看出,当Thread7/Thread10回到线程池后,它们所存储的_counter值并没有被清除,下次继续使用Thread7/Thread10两个线程时,会累加上记录的值。

综上所述,ThreadLocal<T>似乎可以完全替代ThreadStaticAttribute进行使用,当然,如果你使用的是.NET Framework 4.0以下的版本,则只能使用ThreadStaticAttribute

异步编程的线程间数据共享

无论是ThreadLocal<T>还是ThreadStaticAttribute,都无法解决异步编程时的线程数据共享,考虑如下代码:

class Program
{
    internal static ThreadLocal<int> _counter = new ThreadLocal<int>();
    static async Task Main(string[] args)
    {
        await AsyncMethodA();
    }

    static async Task AsyncMethodA()
    {
        _counter.Value = 10;
        Console.WriteLine($"Thread{Thread.CurrentThread.ManagedThreadId} Main Thread counter: {_counter.Value}");
        await AsyncMethodB();
    }

    static async Task AsyncMethodB()
    {
        await Task.Delay(1000);
        Console.WriteLine($"Thread{Thread.CurrentThread.ManagedThreadId} After Delay counter: {_counter.Value}");
    }
}
// 输出:
// Thread1 Main Thread counter: 10
// Thread4 After Delay counter: 0

通过输出可以看出,因为异步编程会导致线程上下文切换,所以ThreadLocal<T>无法在异步中保证数据共享。
解决此问题我们可以使用AsyncLocal<T>将值传递给线程切换上下文,继续流向下面的线程,样例代码:

class Program
{
    internal static AsyncLocal<int> _counter = new AsyncLocal<int>();
    static async Task Main(string[] args)
    {
        await AsyncMethodA();
        await AsyncMethodC();
    }

    static async Task AsyncMethodA()
    {
        _counter.Value = 10;
        Console.WriteLine($"Thread{Thread.CurrentThread.ManagedThreadId} Main Thread counter: {_counter.Value}");
        await AsyncMethodB();
    }

    static async Task AsyncMethodB()
    {
        await Task.Delay(1000);
        Console.WriteLine($"Thread{Thread.CurrentThread.ManagedThreadId} After Delay counter: {_counter.Value}");
    }

    static async Task AsyncMethodC()
    {
        await Task.Delay(1000);
        Console.WriteLine($"Thread{Thread.CurrentThread.ManagedThreadId} AsyncMethodC counter: {_counter.Value}");
    }
}
// 输出:
// Thread1 Main Thread counter: 10
// Thread4 After Delay counter: 10
// Thread4 AsyncMethodC counter: 0

从输出可以看到,Thread1发生线程切换时,将_counter值流向了下面的线程既Thread4

Reference

阅读更多

以太坊黑暗森林-抢跑(front running)

以太坊黑暗森林-抢跑(front running)

前言 鸽了很久之后的今天突然心血来潮,准备写一个系列:以太坊黑暗森林,它介绍以太坊生态上的各种奇思妙想和逆天的攻击方式,会从简单的、常见的攻击方式开始介绍。取这个名字是因为我接触以太坊不久后看的一篇文章 Ethereum is a Dark Forest ,让我想起了《三体》小说中刘慈欣描述的黑暗森林,以太坊是一个弱肉强食的、没有规则的世界,猎人们总是躲在背后监听所有的交易,一旦发现猎物,它们会把它的血给吸干。 开盘抢币 相信进入以太坊生态的韭菜们,一定有过在 uniswap 上买刚开盘新币的经历,新开盘的币,一般会上涨几倍甚至十几倍,越早买入则越能低价买入。你守着时间,等着项目方添加流动性后第一时间买入代币,但是你发现,无论你的手速多块,总是看到一开盘,价格已经飚了几倍,你骂骂咧咧,开始不断拉高 gas 费用,尝试继续买入,但是你眼睁睁的看着代币涨到十倍,自己的交易却一直失败,你开始怀疑项目方自己抢跑,怀疑项目方捣鬼:肯定是项目方吃相难看,用老鼠仓提前买了。另一些聪明人,研究了以太坊的基本技术,他们在 ethscan

By FatTiger
C#:IDisposable 和 析构函数

C#:IDisposable 和 析构函数

C# 中有两种释放资源的方式:实现 IDisposable 或使用析构函数。通常,必须在特定时间释放资源的场景中,我们实现 IDisposable,像这样: public class ExampleDispose : IDisposable { // 非托管资源 private IntPtr _handle; // 使用的其它托管资源 private readonly Stream _stream; private bool disposed = false; public ExampleDispose(Stream stream, IntPtr handle) { this._stream = stream; this._handle = handle; } public void Dispose() { if (disposed) { return; } disposed = t

By FatTiger
ThreadLocal引发的灾难

ThreadLocal引发的灾难

在 Java 里有个称之为线程本地变量的类型叫做 ThreadLocal,它与 ThreadLocal 之于 C# 中是一样的作用,可以在线程范围内设置变量,这个变量只会在当前线程可被访问,但是它们有一点不同的是,在 Java 中,当你设置好变量后,在线程使用完毕回到线程池之前,需要手动调用 ThreadLocal.remove() 方法去清除线程本地变量,否则变量随着线程回到线程池,并且在下次使用此线程时此变量继续存在,而在 C# 中,线程回到线程池时会自动清除本地变量,因此无需手动去清除。 我们的业务有这样一个场景:某个业务 UserService 类中,具有多个方法会频繁(甚至循环)调用一个获取用户标签的接口,具体原因是因为某些方法会进行递归,数据结构有个树状结构,因此,为了优化接口响应时间以及看起来不那么蠢,我使用 ThreadLocal 将用户标签接口的返回数据存储到当前线程,因为在单个请求中,多次调用此接口获取数据是不必要的,它看起来像这样: /** * 此静态变量ThreadLocal会为每个线程创建本地副本, 因此USER_TAGS_THREAD_

By FatTiger
我在币安智能链的日子-区块链基础

我在币安智能链的日子-区块链基础

区块和链 无论是比特币还是以太坊,都是具有一个个区块(称之为Block)的链式结构,学过<数据结构>的肯定明白链表,区块链就像一个链表,每个区块都存储上一个区块哈希。 链(称之为Chain),有非常多的链,他们的协议不同,技术也不尽相同,比特币网络是一个链,以太坊网络是另一个链,每个链都有自己的目标(甚至目标只是为了圈钱),每个链也都有自己的代币,比特币网络的代币是比特币,每次交易都需要比特币作为手续费,以太坊网络代币是以太币,每次在以太坊网络的交易都需要以太币作为手续费。所以,链实际上作为基础设施,非常多的团队喜欢创建新的链,但是一个链光有网络光有代币不行,没有生态,很难成功。 币安智能链(Binance Smart Chain:BSC) 我的主要操作都是在BSC上,没有其它原因,只因为一个穷字。在BTC网络交易,需要BTC用作手续费,这个我可用不起,在以太坊(Ethereum)网络交易,需要以太币(ETH)作为用作手续费,按照以太币目前(

By FatTiger