.NET AsyncLocal 避坑指南(一)


风晓
风晓 2023-12-31 09:23:13 52139 赞同 0 反对 0
分类: 资源
通过 AsyncLocal 我们可以在一个逻辑上下文中维护一份私有数据,该上下文后续代码中都可以访问和修改这份数据,但另一个无关的上下文是无法访问的。

无论是在新创建的 Task 中还是 await 关键词之后,我们都能够访问前面设置的 AsyncLocal 的数据。

class Program
{
    private static AsyncLocal<string> _asyncLocal = new AsyncLocal<string>();
    
    static async Task Main(string[] args)
    {
        _asyncLocal.Value = "Hello World!";
        Task.Run(() => Console.WriteLine($"AsyncLocal in task: {_asyncLocal.Value}"));

        await FooAsync();
        Console.WriteLine($"AsyncLocal after await FooAsync: {_asyncLocal.Value}");
    }

    private static async Task FooAsync()
    {
        await Task.Delay(100);
        Console.WriteLine($"AsyncLocal after await in FooAsync: {_asyncLocal.Value}");
    }
}

输出结果:

AsyncLocal in task: Hello World!
AsyncLocal after await in FooAsync: Hello World!
AsyncLocal after await FooAsync: Hello World!

AsyncLocal 实现原理

在我之前的博客 揭秘 .NET 中的 AsyncLocal 中深入介绍了 AsyncLocal 的实现原理,这里只做简单的回顾。

AsyncLocal 的实际数据存储在 ExecutionContext 中,而 ExecutionContext 作为线程的私有字段与线程绑定,在线程会发生切换的地方,runtime 会将切换前的 ExecutionContext 保存起来,切换后再恢复到新线程上。

这个保存和恢复的过程是由 runtime 自动完成的,例如会发生在以下几个地方:

  • new Thread(ThreadStart start).Start()
  • Task.Run(Action action)
  • ThreadPool.QueueUserWorkItem(WaitCallback callBack)
  • await 之后

以 await 为例,当我们在一个方法中使用了 await 关键词,编译器会将这个方法编译成一个状态机,这个状态机会在 await 之前和之后分别保存和恢复 ExecutionContext。

class Program
{
    private static AsyncLocal<string> _asyncLocal = new AsyncLocal<string>();
    
    static async Task Main(string[] args)
    {
        _asyncLocal.Value = "Hello World!";
        await FooAsync();
        Console.WriteLine($"AsyncLocal after await FooAsync: {_asyncLocal.Value}");
    }

    private static async Task FooAsync()
    {
        await Task.Delay(100);
    }
}

输出结果:

AsyncLocal after await FooAsync: Hello World!

AsyncLocal 的坑

有时候我们会在 FooAsync 方法中去修改 AsyncLocal 的值,并希望在 Main 方法在 await FooAsync 之后能够获取到修改后的值,但是实际上这是不可能的。

class Program
{
    private static AsyncLocal<string> _asyncLocal = new AsyncLocal<string>();
    
    static async Task Main(string[] args)
    {
        _asyncLocal.Value = "A";
        Console.WriteLine($"AsyncLocal before FooAsync: {_asyncLocal.Value}");
        await FooAsync();
        Console.WriteLine($"AsyncLocal after await FooAsync: {_asyncLocal.Value}");
    }

    private static async Task FooAsync()
    {
        _asyncLocal.Value = "B";
        Console.WriteLine($"AsyncLocal before await in FooAsync: {_asyncLocal.Value}");
        await Task.Delay(100);
        Console.WriteLine($"AsyncLocal after await in FooAsync: {_asyncLocal.Value}");
    }
}

输出结果:

AsyncLocal before FooAsync: A
AsyncLocal before await in FooAsync: B
AsyncLocal after await in FooAsync: B
AsyncLocal after await FooAsync: A

为什么我们在 FooAsync 方法中修改了 AsyncLocal 的值,但是在 await FooAsync 之后,AsyncLocal 的值却没有被修改呢?

原因是 ExecutionContext 被设计成了一个不可变的对象,当我们在 FooAsync 方法中修改了 AsyncLocal 的值,实际上是创建了一个新的 ExecutionContext,原来其他的 AsyncLocal 的值被值拷贝到了新的 ExecutionContext 中,新的 AsyncLocal 的值只会写入到新的 ExecutionContext 中,而原来的 ExecutionContext 及其关联的 AsyncLocal 仍然保持不变。

这样的设计是为了保证线程的安全性,因为在多线程环境下,如果 ExecutionContext 是可变的,那么在切换线程的时候,可能会出现数据不一致的情况。

我们通常把这种设计称为 Copy On Write(简称COW),即在修改数据的时候,会先拷贝一份数据,然后在拷贝的数据上进行修改,这样就不会影响到原来的数据。

ExecutionContext 中可能不止一个 AsyncLocal 的数据,修改任意一个 AsyncLocal 都会导致 ExecutionContext 的 COW。

如果您发现该资源为电子书等存在侵权的资源或对该资源描述不正确等,可点击“私信”按钮向作者进行反馈;如作者无回复可进行平台仲裁,我们会在第一时间进行处理!

评价 0 条
风晓L1
粉丝 1 资源 2038 + 关注 私信
最近热门资源
银河麒麟桌面操作系统备份用户数据  129
统信桌面专业版【全盘安装UOS系统】介绍  128
银河麒麟桌面操作系统安装佳能打印机驱动方法  119
银河麒麟桌面操作系统 V10-SP1用户密码修改  108
麒麟系统连接打印机常见问题及解决方法  21
最近下载排行榜
银河麒麟桌面操作系统备份用户数据 0
统信桌面专业版【全盘安装UOS系统】介绍 0
银河麒麟桌面操作系统安装佳能打印机驱动方法 0
银河麒麟桌面操作系统 V10-SP1用户密码修改 0
麒麟系统连接打印机常见问题及解决方法 0
作者收入月榜
1

prtyaa 收益393.62元

2

zlj141319 收益218元

3

1843880570 收益214.2元

4

IT-feng 收益210.13元

5

风晓 收益208.24元

6

777 收益172.71元

7

Fhawking 收益106.6元

8

信创来了 收益105.84元

9

克里斯蒂亚诺诺 收益91.08元

10

技术-小陈 收益79.5元

请使用微信扫码

加入交流群

请使用微信扫一扫!