AsyncLocal 用法简介

通过 AsyncLocal 我们可以在一个逻辑上下中维护一份数据,并且在后续代码中都可以访问和修改这份数据。

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

  1. class Program
  2. {
  3. private static AsyncLocal<string> _asyncLocal = new AsyncLocal<string>();
  4. static async Task Main(string[] args)
  5. {
  6. _asyncLocal.Value = "Hello World!";
  7. Task.Run(() => Console.WriteLine($"AsyncLocal in task: {_asyncLocal.Value}"));
  8. await FooAsync();
  9. Console.WriteLine($"AsyncLocal after await FooAsync: {_asyncLocal.Value}");
  10. }
  11. private static async Task FooAsync()
  12. {
  13. await Task.Delay(100);
  14. Console.WriteLine($"AsyncLocal after await in FooAsync: {_asyncLocal.Value}");
  15. }
  16. }

输出结果:

  1. AsyncLocal in task: Hello World!
  2. AsyncLocal after await in FooAsync: Hello World!
  3. 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。

  1. class Program
  2. {
  3. private static AsyncLocal<string> _asyncLocal = new AsyncLocal<string>();
  4. static async Task Main(string[] args)
  5. {
  6. _asyncLocal.Value = "Hello World!";
  7. await FooAsync();
  8. Console.WriteLine($"AsyncLocal after await FooAsync: {_asyncLocal.Value}");
  9. }
  10. private static async Task FooAsync()
  11. {
  12. await Task.Delay(100);
  13. }
  14. }

输出结果:

  1. AsyncLocal after await FooAsync: Hello World!

AsyncLocal 的坑

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

  1. class Program
  2. {
  3. private static AsyncLocal<string> _asyncLocal = new AsyncLocal<string>();
  4. static async Task Main(string[] args)
  5. {
  6. _asyncLocal.Value = "A";
  7. Console.WriteLine($"AsyncLocal before FooAsync: {_asyncLocal.Value}");
  8. await FooAsync();
  9. Console.WriteLine($"AsyncLocal after await FooAsync: {_asyncLocal.Value}");
  10. }
  11. private static async Task FooAsync()
  12. {
  13. _asyncLocal.Value = "B";
  14. Console.WriteLine($"AsyncLocal before await in FooAsync: {_asyncLocal.Value}");
  15. await Task.Delay(100);
  16. Console.WriteLine($"AsyncLocal after await in FooAsync: {_asyncLocal.Value}");
  17. }
  18. }

输出结果:

  1. AsyncLocal before FooAsync: A
  2. AsyncLocal before await in FooAsync: B
  3. AsyncLocal after await in FooAsync: B
  4. AsyncLocal after await FooAsync: A

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

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

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

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

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

所以上面代码的执行过程如下:

AsyncLocal 的避坑指南

那么我们如何在 FooAsync 方法中修改 AsyncLocal 的值,并且在 Main 方法中获取到修改后的值呢?

我们需要借助一个中介者,让中介者来保存 AsyncLocal 的值,然后在 FooAsync 方法中修改中介者的属性值,这样就可以在 Main 方法中获取到修改后的值了。

下面我们设计一个 ValueHolder 来保存 AsyncLocal 的值,修改 Value 并不会修改 AsyncLocal 的值,而是修改 ValueHolder 的属性值,这样就不会触发 ExecutionContext 的 COW。

我们还需要设计一个 ValueAccessor 来封装 ValueHolder 对值的访问和修改,这样可以保证 ValueHolder 的值只能在 ValueAccessor 中被修改。

  1. class ValueAccessor<T> : IValueAccessor<T>
  2. {
  3. private static AsyncLocal<ValueHolder<T>> _asyncLocal = new AsyncLocal<ValueHolder<T>>();
  4. public T Value
  5. {
  6. get => _asyncLocal.Value != null ? _asyncLocal.Value.Value : default;
  7. set
  8. {
  9. _asyncLocal.Value ??= new ValueHolder<T>();
  10. _asyncLocal.Value.Value = value;
  11. }
  12. }
  13. }
  14. class ValueHolder<T>
  15. {
  16. public T Value { get; set; }
  17. }
  18. class Program
  19. {
  20. private static IValueAccessor<string> _valueAccessor = new ValueAccessor<string>();
  21. static async Task Main(string[] args)
  22. {
  23. _valueAccessor.Value = "A";
  24. Console.WriteLine($"ValueAccessor before await FooAsync in Main: {_valueAccessor.Value}");
  25. await FooAsync();
  26. Console.WriteLine($"ValueAccessor after await FooAsync in Main: {_valueAccessor.Value}");
  27. }
  28. private static async Task FooAsync()
  29. {
  30. _valueAccessor.Value = "B";
  31. Console.WriteLine($"ValueAccessor before await in FooAsync: {_valueAccessor.Value}");
  32. await Task.Delay(100);
  33. Console.WriteLine($"ValueAccessor after await in FooAsync: {_valueAccessor.Value}");
  34. }
  35. }

输出结果:

  1. ValueAccessor before await FooAsync in Main: A
  2. ValueAccessor before await in FooAsync: B
  3. ValueAccessor after await in FooAsync: B
  4. ValueAccessor after await FooAsync in Main: B

HttpContextAccessor 的实现原理

我们常用的 HttpContextAccessor 通过HttpContextHolder 来间接地在 AsyncLocal 中存储 HttpContext。

如果要更新 HttpContext,只需要在 HttpContextHolder 中更新即可。因为 AsyncLocal 的值不会被修改,更新 HttpContext 时 ExecutionContext 也不会出现 COW 的情况。

不过 HttpContextAccessor 中的逻辑有点特殊,它的 HttpContextHolder 是为保证清除 HttpContext 时,这个 HttpContext 能在所有引用它的 ExecutionContext 中被清除(可能因为修改 HttpContextHolder 之外的 AsyncLocal 数据导致 ExecutionContext 已经 COW 很多次了)。

下面是 HttpContextAccessor 的实现,英文注释是原文,中文注释是我自己的理解。

  1. /// </summary>
  2. public class HttpContextAccessor : IHttpContextAccessor
  3. {
  4. private static readonly AsyncLocal<HttpContextHolder> _httpContextCurrent = new AsyncLocal<HttpContextHolder>();
  5. /// <inheritdoc/>
  6. public HttpContext? HttpContext
  7. {
  8. get
  9. {
  10. return _httpContextCurrent.Value?.Context;
  11. }
  12. set
  13. {
  14. var holder = _httpContextCurrent.Value;
  15. if (holder != null)
  16. {
  17. // Clear current HttpContext trapped in the AsyncLocals, as its done.
  18. // 这边的逻辑是为了保证清除 HttpContext 时,这个 HttpContext 能在所有引用它的 ExecutionContext 中被清除
  19. holder.Context = null;
  20. }
  21. if (value != null)
  22. {
  23. // Use an object indirection to hold the HttpContext in the AsyncLocal,
  24. // so it can be cleared in all ExecutionContexts when its cleared.
  25. // 这边直接修改了 AsyncLocal 的值,所以会导致 ExecutionContext 的 COW。新的 HttpContext 不会被传递到原先的 ExecutionContext 中。
  26. _httpContextCurrent.Value = new HttpContextHolder { Context = value };
  27. }
  28. }
  29. }
  30. private sealed class HttpContextHolder
  31. {
  32. public HttpContext? Context;
  33. }
  34. }

但 HttpContextAccessor 的实现并不允许将新赋值的非 null 的 HttpContext 传递到外层的 ExecutionContext 中,可以参考上面的源码及注释理解。

  1. class Program
  2. {
  3. private static IHttpContextAccessor _httpContextAccessor = new HttpContextAccessor();
  4. static async Task Main(string[] args)
  5. {
  6. var httpContext = new DefaultHttpContext
  7. {
  8. Items = new Dictionary<object, object>
  9. {
  10. { "Name", "A"}
  11. }
  12. };
  13. _httpContextAccessor.HttpContext = httpContext;
  14. Console.WriteLine($"HttpContext before await FooAsync in Main: {_httpContextAccessor.HttpContext.Items["Name"]}");
  15. await FooAsync();
  16. // HttpContext 被清空了,下面这行输出 null
  17. Console.WriteLine($"HttpContext after await FooAsync in Main: {_httpContextAccessor.HttpContext?.Items["Name"]}");
  18. }
  19. private static async Task FooAsync()
  20. {
  21. _httpContextAccessor.HttpContext = new DefaultHttpContext
  22. {
  23. Items = new Dictionary<object, object>
  24. {
  25. { "Name", "B"}
  26. }
  27. };
  28. Console.WriteLine($"HttpContext before await in FooAsync: {_httpContextAccessor.HttpContext.Items["Name"]}");
  29. await Task.Delay(1000);
  30. Console.WriteLine($"HttpContext after await in FooAsync: {_httpContextAccessor.HttpContext.Items["Name"]}");
  31. }
  32. }

输出结果:

  1. HttpContext before await FooAsync in Main: A
  2. HttpContext before await in FooAsync: B
  3. HttpContext after await in FooAsync: B
  4. HttpContext after await FooAsync in Main:

.NET AsyncLocal 避坑指南的更多相关文章

  1. electron 编译 sqlite3避坑指南---尾部链接有已经编译成功的sqlite3

    electron 编译 sqlite3避坑指南(尾部链接有已经编译成功的sqlite3) sqlite很好用,不需要安装,使用electron开发桌面程序,sqlite自然是存储数据的不二之选,奈何编 ...

  2. CEF避坑指南(一)——下载并编译第一个示例

    CEF即Chromium Embedded Framework,Chrome浏览器嵌入式框架.它提供了接口供程序员们把Chrome放到自己的程序中.许多大型公司,如网易.腾讯都开始使用CEF进行前端开 ...

  3. Canal v1.1.4版本避坑指南

    前提 在忍耐了很久之后,忍不住爆发了,在掘金发了条沸点(下班时发的): 这是一个令人悲伤的故事,这条情感爆发的沸点好像被屏蔽了,另外小水渠(Canal意为水道.管道)上线一段时间,不出坑的时候风平浪静 ...

  4. Linux下Python3.6的安装及避坑指南

    Python3的安装 1.安装依赖环境 Python3在安装的过程中可能会用到各种依赖库,所以在正式安装Python3之前,需要将这些依赖库先行安装好. yum -y install zlib-dev ...

  5. Hive改表结构的两个坑|避坑指南

    Hive在大数据中可能是数据工程师使用的最多的组件,常见的数据仓库一般都是基于Hive搭建的,在使用Hive时候,遇到了两个奇怪的现象,今天给大家聊一下,以后遇到此类问题知道如何避坑! 坑一:改变字段 ...

  6. Harmony OS 开发避坑指南——源码下载和编译

    Harmony OS 开发避坑指南--源码下载和编译 本文介绍了如何下载鸿蒙系统源码,如何一次性配置可以编译三个目标平台(Hi3516,Hi3518和Hi3861)的编译环境,以及如何将源码编译为三个 ...

  7. 今天 1024,为了不 996,Lombok 用起来以及避坑指南

    Lombok简介.使用.工作原理.优缺点 Lombok 项目是一个 Java 库,它会自动插入编辑器和构建工具中,Lombok 提供了一组有用的注解,用来消除 Java 类中的大量样板代码. 目录 L ...

  8. Android连接远程数据库的避坑指南

    Android连接远程数据库的避坑指南 今天用Android Studio连接数据库时候,写了个测试连接的按钮,然后连接的时候报错了,报错信息: 2021-09-07 22:45:20.433 705 ...

  9. Windows环境下Anaconda安装TensorFlow的避坑指南

    最近群里聊天时经常会提到DL的东西,也有群友在学习mxnet,但听说坑比较多.为了赶上潮流顺便避坑,我果断选择了TensorFlow,然而谁知一上来就掉坑里了…… 我根据网上的安装教程,默认安装了最新 ...

  10. spring-boot-starter-thymeleaf 避坑指南

    第一步:pom配置环境 先不要管包是做什么的 总之必须要有 否则进坑 <!--避坑包--> <dependency> <groupId>net.sourceforg ...

随机推荐

  1. 《MySQL必知必会》之快速入门存储过程

    使用存储过程 本章介绍什么是存储过程,为什么使用.如何使用,并介绍如何创建和使用存储过程的基本语法 存储过程 在实际应用中,往往需要执行多个表的多条sql语句 存储过程就是为以后的使用而保存的一条或者 ...

  2. Vue快速上门(1)-基础知识图文版

    VUE家族系列: Vue快速上门(1)-基础知识 Vue快速上门(2)-模板语法 Vue快速上门(3)-组件与复用 01.基本概念 1.1.先了解下MVVM VUE是基于MVVM思想实现的,那什么是M ...

  3. 《HTTP权威指南》 – 11.验证码和新鲜度

    服务器应当告知客户端能够将内容缓存多长时间,在这个时间内就是新鲜的.服务器可以用这两个首部之一来提供信息: Expires(过期) Cache - Control(缓存控制) Expires首部 规定 ...

  4. python之yaml文件读取封装

    import os import yaml from yamlinclude import YamlIncludeConstructor YamlIncludeConstructor.add_to_l ...

  5. 有备无患!DBS高性价比方案助力富途证券备份上云

    "某中心受病毒攻击,导致服务中断,线上业务被迫暂停" "某公司员工误操作删库,核心业务数据部分丢失,无法完全找回" "由于服务器断线,某医院信息系统瘫 ...

  6. 【转载】SQL SERVER 表变量与临时表的优缺点

    什么情况下使用表变量?什么情况下使用临时表? -- 表变量: DECLARE @tb table(id int identity(1,1), name varchar(100)) INSERT @tb ...

  7. 用友开发者中心全新升级,YonBuilder移动开发入门指南

    听说用友新上线了全新的开发者中心,有YonBuilder应用开发,集成开发.数据开发.智能与自动化.DevOps 等板块,本人作为用户老客户,对其中的移动开发比较感兴趣,本文重点讲解其中的移动开发平台 ...

  8. RocketMQ Compaction Topic的设计与实现

    本文作者:刘涛,阿里云智能技术专家. 01 Compaction Topic介绍 一般来说,消息队列提供的数据过期机制有如下几种,比如有基于时间的过期机制--数据保存多长时间后即进行清理,也有基于数据 ...

  9. C语言常用知识总结

    在 C 语言中,常量是一种固定值的标识符,它的值在程序执行期间不会改变. C 语言中有几种不同类型的常量: 字符常量:用单引号括起来的单个字符,例如 'A'.'b'.'1' 等. 字符串常量:用双引号 ...

  10. [Unity]限制两个物体之间的距离

    //限制两个物体之间的距离 if (Vector3.Distance(B.position, A.position) > maxDistance) { //获得两个物体之间的单位向量 Vecto ...