.NET的AsyncLocal用法指南

news/2023/12/1 1:41:45

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实现原理

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。

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

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 被清空了,下面这行输出 nullConsole.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: 

http://www.ppmy.cn/news/470573.html

相关文章

51单片机 - 期末复习重要图

AT89S51片内硬件结构 1.内部硬件结构图 2.内部部件简单介绍 3. 21个特殊功能寄存器分类 按照定时器、串口、通用I/O口和CPU 中断相关寄存器&#xff1a;3IE - 中断使能寄存器IP - 中断优先级寄存器EA - 全局中断使能位 定时器相关寄存器6TCON - 定时器/计数器控制寄存器TMO…

level12

根据上一题&#xff0c;先看看源代码 User-Agent: " type"text" onclick"alert(xss)

l2行情是什么呢?

l2行情是上海证券交易所发布的高速行情。速度比普通软件快3&#xff0d;10秒&#xff0c;适合做短线。能显示20档买卖盘口&#xff0c;普通只能显10档。而且是逐笔成交显示&#xff0c;比如普通软件显示成交了1000手&#xff0c;而L2可以细分这1000手里面共有多少个单成交&…

汽车理论习题1.3

汽车理论习题1.3 #-*- coding: utf-8 -*- import matplotlib.pyplot as plt import numpy as np import math#绘制坐标轴以及标明xy轴 plt.rcParams[font.sans-serif][SimHei] plt.rcParams[axes.unicode_minus]False fig1 plt.figure(1) ax1 fig1.add_subplot(111) ax1.set…

L2-2 抢红包

没有人没抢过红包吧…… 这里给出N个人之间互相发红包、抢红包的记录&#xff0c;请你统计一下他们抢红包的收获。 输入格式&#xff1a; 输入第一行给出一个正整数N&#xff08;≤104&#xff09;&#xff0c;即参与发红包和抢红包的总人数&#xff0c;则这些人从1到N编号。…

VLQ的介绍

本文来介绍一下VLQ&#xff08;variable-length quantity) Wiki&#xff1a;Variable-length quantity 用变长字节数组来对整形进行编码&#xff0c;极大提高存储效率。 其实&#xff0c;这样的优点也被应用到了protobuf中&#xff0c;所以才有较高的序列化效率。 Varints in …

lq到底是什么意思_lq是什么意思

2018-03-27 lq是什么意思 【商业】   导商(Leading Quotient——LQ)。导商即为领导商&#xff0c;是指一个人领导、指导、引导、带领他人或团队组织的智慧和能力的商数。导商既取决于领导理论与方式&#xff0c;又取决于领导环境与气氛&#xff1b;既取决于领导特质与人格&am…

数据结构队列基本实现

队列是一种操作受限的线性表。在这种线性表上&#xff0c;插入操作限定在表的某一端进行&#xff0c;删除限定在表的另一端进行。允许插入的一端为队尾&#xff0c;允许删除的一端为对头。队列按照存储方式分为两种&#xff0c;分别是顺序存储和链式存储。其中顺序存储方式的队…

二、LINQ

文章目录 一、LINQ概述与查询语法二、LINQ方法语法基础三、LINQ聚合操作与元素操作四、数据类型转换 LINQ&#xff08;Language Integrated Query&#xff0c;语言集成查询&#xff09;&#xff0c;可为C#语法提供强大的查询功能。 一、LINQ概述与查询语法 LINQ提供了一种跨数…

L2-2 列车调度

火车站的列车调度铁轨的结构如下图所示。 两端分别是一条入口&#xff08;Entrance&#xff09;轨道和一条出口&#xff08;Exit&#xff09;轨道&#xff0c;它们之间有N条平行的轨道。每趟列车从入口可以选择任意一条轨道进入&#xff0c;最后从出口离开。在图中有9趟列车&am…

RLx2~~

大模型时代&#xff0c;模型压缩和加速显得尤为重要。传统监督学习可通过稀疏神经网络实现模型压缩和加速&#xff0c;那么同样需要大量计算开销的强化学习任务可以基于稀疏网络进行训练吗&#xff1f;本文提出了一种强化学习专用稀疏训练框架&#xff0c;可以节省至多 95% 的训…

DQL2

/* DQL标准语法结构:编写DQL一定要严格按照此语法的顺序来实现&#xff01; SELECT [ALL | DISTINCT] ALL表示查询出所有的内容 DISTINCT 去重 {* | 表名.* | 表名.字段名[ AS 别名][,…]} 指定查询出的字段的 FROM 表名[AS 别名][,表1… AS 别名] [INNER | [LEFT | RIGHT] [OU…

Hql(01)

一&#xff1a;什么是Hql HQL是Hibernate Query Language的缩写&#xff0c;提供更加丰富灵活、更为强大的查询能力&#xff1b;HQL更接近SQL语句查询语法。 二&#xff1a;hql和sql区别/异同&#xff08;面试题&#xff09; HQL S…

HQL(二)

一、写BaseDao 需求&#xff1a; 按名字分页查询对应书籍信息 我们用hql是想我们的需求&#xff0c;是这样的&#xff1a; public List<Book> list1(Book book,PageBean pageBean){Session session SessionFactoryUtils.getSession();Transaction transaction sess…

蓝桥杯Java知识准备

1.输入输出 提到了几个IO类&#xff0c;这里推荐使用BufferedReader输入&#xff0c;BufferedWriter输出&#xff0c;当输入输出的数据量大于一百万左右就必须使用快速IO不能直接使用Scanne和System.out.print。 1.1 正常输入输出 输入 首先定义一个Scanner对象&#xff0c…

lq到底是什么意思_LQ是什么意思..?!谁知道..!?

导航:网站首页 > LQ是什么意思..?!谁知道..!? LQ是什么意思..?!谁知道..!? 匿名网友: 导商Leading Quotient——LQ。 导商即为领导商,是指一个人领导、指导、引导、带领他人或团队组织的智慧和能力的商数。 导商既取决于领导理论与方式,又取决于领导环境与气氛;既取…

qml-2 定位

xyz布局定位 继承体系 Button ->AbstractButton->Control->Item->QQuickItem 介绍&#xff1a;The QQuickItem class provides the most basic of all visual items in Qt Quick 属性&#xff1a; x : qreal Defines the items x position relative to its pa…

并行Linq

并行LINQ 并行查询 .NET4在System.Linq名称空间中包含一个新类ParalleIEnumerable ,可以分解查询的工作使其分布在多个线程上。尽管Enmerable类给IEnunerable<T>接口定义了扩展方法,但 ParalleIEnumerable 类的大多数扩展方法是ParallelQuery<TSource>类的扩展。…

L1、L2的作用

L范式都是为了防止模型过拟合&#xff0c;所谓范式就是加入参数的约束。 L1的作用是为了矩阵稀疏化。假设的是模型的参数取值满足拉普拉斯分布。 L2的作用是为了使模型更平滑&#xff0c;得到更好的泛化能力。假设的是参数是满足高斯分布。 借用公众号python与算法社区的内容20…

PLSQL(二)

PLSQL(二&#xff09; 通过本文将学习到 NULL的判断IF控制语句循环控制语句定义复杂类型游标的处理例外的声明函数与存储过程的使用PLSQL中程序包的作用 2、NULL的判断 我一直以为NULL读nang&#xff0c;因为从我开始学计算机的时候别人就这么读。但是我昨天听到有人读no&a…
最新文章