AsyncLocal 用法简介

通过 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 中,而原来的 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 中被修改。

class ValueAccessor<T> : IValueAccessor<T>
{
private static AsyncLocal<ValueHolder<T>> _asyncLocal = new AsyncLocal<ValueHolder<T>>(); public T Value
{
get => _asyncLocal.Value != null ? _asyncLocal.Value.Value : default;
set
{
_asyncLocal.Value ??= new ValueHolder<T>(); _asyncLocal.Value.Value = value;
}
}
} class ValueHolder<T>
{
public T Value { get; set; }
} class Program
{
private static IValueAccessor<string> _valueAccessor = new ValueAccessor<string>(); static async Task Main(string[] args)
{
_valueAccessor.Value = "A";
Console.WriteLine($"ValueAccessor before await FooAsync in Main: {_valueAccessor.Value}");
await FooAsync();
Console.WriteLine($"ValueAccessor after await FooAsync in Main: {_valueAccessor.Value}");
} private static async Task FooAsync()
{
_valueAccessor.Value = "B";
Console.WriteLine($"ValueAccessor before await in FooAsync: {_valueAccessor.Value}");
await Task.Delay(100);
Console.WriteLine($"ValueAccessor after await in FooAsync: {_valueAccessor.Value}");
}
}

输出结果:

ValueAccessor before await FooAsync in Main: A
ValueAccessor before await in FooAsync: B
ValueAccessor after await in FooAsync: B
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 的实现,英文注释是原文,中文注释是我自己的理解。

/// </summary>
public class HttpContextAccessor : IHttpContextAccessor
{
private static readonly AsyncLocal<HttpContextHolder> _httpContextCurrent = new AsyncLocal<HttpContextHolder>(); /// <inheritdoc/>
public HttpContext? HttpContext
{
get
{
return _httpContextCurrent.Value?.Context;
}
set
{
var holder = _httpContextCurrent.Value;
if (holder != null)
{
// Clear current HttpContext trapped in the AsyncLocals, as its done.
// 这边的逻辑是为了保证清除 HttpContext 时,这个 HttpContext 能在所有引用它的 ExecutionContext 中被清除
holder.Context = null;
} if (value != null)
{
// Use an object indirection to hold the HttpContext in the AsyncLocal,
// so it can be cleared in all ExecutionContexts when its cleared.
// 这边直接修改了 AsyncLocal 的值,所以会导致 ExecutionContext 的 COW。新的 HttpContext 不会被传递到原先的 ExecutionContext 中。
_httpContextCurrent.Value = new HttpContextHolder { Context = value };
}
}
} private sealed class HttpContextHolder
{
public HttpContext? Context;
}
}

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

class Program
{
private static IHttpContextAccessor _httpContextAccessor = new HttpContextAccessor(); static async Task Main(string[] args)
{
var httpContext = new DefaultHttpContext
{
Items = new Dictionary<object, object>
{
{ "Name", "A"}
}
};
_httpContextAccessor.HttpContext = httpContext;
Console.WriteLine($"HttpContext before await FooAsync in Main: {_httpContextAccessor.HttpContext.Items["Name"]}");
await FooAsync();
// HttpContext 被清空了,下面这行输出 null
Console.WriteLine($"HttpContext after await FooAsync in Main: {_httpContextAccessor.HttpContext?.Items["Name"]}");
} private static async Task FooAsync()
{
_httpContextAccessor.HttpContext = new DefaultHttpContext
{
Items = new Dictionary<object, object>
{
{ "Name", "B"}
}
};
Console.WriteLine($"HttpContext before await in FooAsync: {_httpContextAccessor.HttpContext.Items["Name"]}");
await Task.Delay(1000);
Console.WriteLine($"HttpContext after await in FooAsync: {_httpContextAccessor.HttpContext.Items["Name"]}");
}
}

输出结果:

HttpContext before await FooAsync in Main: A
HttpContext before await in FooAsync: B
HttpContext after await in FooAsync: B
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. Django静态文件配置(from表单-request对象方法-get请求与post请求-视图函数书写)

    目录 一:静态文件配置 1.静态文件配置 2.什么是静态文件? 3.静态文件的创建 4.解决使用django后端代码修改前端不显示(缓存问题) 5.settings.py 静态文件配置 6.静态文接口 ...

  2. Introduction & Directory

    一个日常划水的高中生而已啦,我不会承认这个博客的CSS是copy的 一个貌似并不准确的图-- <算法竞赛进阶指南>学习: 动态规划: DP经典例题--LIS&LCS 洛谷题解: 搜 ...

  3. Vue3 企业级优雅实战 - 组件库框架 - 9 实现组件库 cli - 上

    上文搭建了组件库 cli 的基础架子,实现了创建组件时的用户交互,但遗留了 cli/src/command/create-component.ts 中的 createNewComponent 函数,该 ...

  4. docker搭建Elasticsearch、Kibana、Logstash 同步mysql数据到ES

    一.前言 在数据量大的企业级实践中,Elasticsearch显得非常常见,特别是数据表超过千万级后,无论怎么优化,还是有点力不从心!使用中,最首先的问题就是怎么把千万级数据同步到Elasticsea ...

  5. sql根据团队树一级一级汇总统计

    ​ 1.需求描述 最近碰到了一个需求,是要统计各个团队的员工的销售金额,然后一级一级向上汇总. ​编辑 架构团队树是类似于这种样子的,需要先算出每个员工的销售金额,然后汇总成上一级的团队金额,然后各个 ...

  6. [常用工具] git基础学习笔记

    git基础学习笔记,参考视频:1小时玩转 Git/Github 添加推送信息,-m= message git commit -m "添加注释" 查看状态 git status 显示 ...

  7. 发布个工具,一键恢复Win8/8.1中的微软拼音长句模式(新体验模式)

    (cnBeta:http://www.cnbeta.com/articles/277936.htm) 首先贴个图,大家来一起念台词~ 念完了木有?很激情澎湃义愤填膺有木有? 这事情最早追溯到前年 8 ...

  8. Spring MVC的运行流程

    Spring MVC的运行流程 摘要:本文档主要结合官方给出的Spring MVC流程图结合一个简单的Spring MVC实例,分析并介绍了Spring MVC的运行流程. 目录 Spring MVC ...

  9. wkhtmltopdf 目录对象使用 及 目录样式分享

    最近又是更新了报告模板的样式新使用了目录对象 因为直接生成的目录样式比较丑所以这边使用的是自定义xsl 直接生成的目录样式 自定义样式 因为需求所以写了些特殊判断 <xsl:if test=&q ...

  10. DVWA靶场实战(十)——XSS(DOM)

    DVWA靶场实战(十) 五.XSS(DOM): 1.漏洞原理: XSS全称为Cross Site Scripting,由于和层叠样式表(Cascading Style Sheets,CSS)重名,所以 ...