前言

在Web 应用程序中,我们经常会遇到这样的场景,如用户信息,租户信息本次的请求过程中都是固定的,我们希望是这种信息在本次请求内,一次赋值,到处使用。本文就来探讨一下,如何在.NET Core 下去利用AsyncLocal 实现全局共享变量。

简介

我们如果需要整个程序共享一个变量,我们仅需将该变量放在某个静态类的静态变量上即可(不满足我们的需求,静态变量上,整个程序都是固定值)。我们在Web 应用程序中,每个Web 请求服务器都为其分配了一个独立线程,如何实现用户,租户等信息隔离在这些独立线程中。这就是今天要说的线程本地存储。针对线程本地存储 .NET 给我们提供了两个类 ThreadLocal 和 AsyncLocal。我们可以通过查看以下例子清晰的看到两者的区别:


[TestClass]
public class TastLocal {
private static ThreadLocal<string> threadLocal = new ThreadLocal<string>();
private static AsyncLocal<string> asyncLocal = new AsyncLocal<string>();
[TestMethod]
public void Test() {
threadLocal.Value = "threadLocal";
asyncLocal.Value = "asyncLocal";
var threadId = Thread.CurrentThread.ManagedThreadId;
Task.Factory.StartNew(() => {
var threadId = Thread.CurrentThread.ManagedThreadId;
Debug.WriteLine($"StartNew:threadId:{ threadId}; threadLocal:{threadLocal.Value}");
Debug.WriteLine($"StartNew:threadId:{ threadId}; asyncLocal:{asyncLocal.Value}");
});
CurrThread();
}
public void CurrThread() {
var threadId = Thread.CurrentThread.ManagedThreadId;
Debug.WriteLine($"CurrThread:threadId:{threadId};threadLocal:{threadLocal.Value}");
Debug.WriteLine($"CurrThread:threadId:{threadId};asyncLocal:{asyncLocal.Value}");
}
}

输出结果:

CurrThread:threadId:4;threadLocal:threadLocal
StartNew:threadId:11; threadLocal:
CurrThread:threadId:4;asyncLocal:asyncLocal
StartNew:threadId:11; asyncLocal:asyncLocal

从上面结果中可以看出 ThreadLocal 和 AsyncLocal 都能实现基于线程的本地存储。但是当线程切换后,只有 AsyncLocal 还能够保留原来的值。在Web 开发中,我们会有很多异步场景,在这些场景下,可能会出现线程的切换。所以我们使用AsyncLocal 去实现在Web 应用程序下的共享变量。

AsyncLocal 解读

  1. 官方文档
  2. 源码地址

源码查看:

public sealed class AsyncLocal<T> : IAsyncLocal
{
private readonly Action<AsyncLocalValueChangedArgs<T>>? m_valueChangedHandler; //
// 无参构造函数
//
public AsyncLocal()
{
} //
// 构造一个带有委托的AsyncLocal<T>,该委托在当前值更改时被调用
// 在任何线程上
//
public AsyncLocal(Action<AsyncLocalValueChangedArgs<T>>? valueChangedHandler)
{
m_valueChangedHandler = valueChangedHandler;
} [MaybeNull]
public T Value
{
get
{
object? obj = ExecutionContext.GetLocalValue(this);
return (obj == null) ? default : (T)obj;
}
set => ExecutionContext.SetLocalValue(this, value, m_valueChangedHandler != null);
} void IAsyncLocal.OnValueChanged(object? previousValueObj, object? currentValueObj, bool contextChanged)
{
Debug.Assert(m_valueChangedHandler != null);
T previousValue = previousValueObj == null ? default! : (T)previousValueObj;
T currentValue = currentValueObj == null ? default! : (T)currentValueObj;
m_valueChangedHandler(new AsyncLocalValueChangedArgs<T>(previousValue, currentValue, contextChanged));
}
} //
// 接口,允许ExecutionContext中的非泛型代码调用泛型AsyncLocal<T>类型
//
internal interface IAsyncLocal
{
void OnValueChanged(object? previousValue, object? currentValue, bool contextChanged);
} public readonly struct AsyncLocalValueChangedArgs<T>
{
public T? PreviousValue { get; }
public T? CurrentValue { get; } //
// If the value changed because we changed to a different ExecutionContext, this is true. If it changed
// because someone set the Value property, this is false.
//
public bool ThreadContextChanged { get; } internal AsyncLocalValueChangedArgs(T? previousValue, T? currentValue, bool contextChanged)
{
PreviousValue = previousValue!;
CurrentValue = currentValue!;
ThreadContextChanged = contextChanged;
}
} //
// Interface used to store an IAsyncLocal => object mapping in ExecutionContext.
// Implementations are specialized based on the number of elements in the immutable
// map in order to minimize memory consumption and look-up times.
//
internal interface IAsyncLocalValueMap
{
bool TryGetValue(IAsyncLocal key, out object? value);
IAsyncLocalValueMap Set(IAsyncLocal key, object? value, bool treatNullValueAsNonexistent);
}

我们知道在.NET 里面,每个线程都关联着执行上下文。我们可以通 Thread.CurrentThread.ExecutionContext 属性进行访问 或者通过 ExecutionContext.Capture() 获取。

从上面我们可以看出 AsyncLocal 的 Value 存取是通过 ExecutionContext.GetLocalValue 和GetLocalValue.SetLocalValue 进行操作的,我们可以继续从 ExecutionContext 里面取出部分代码查看(源码地址),为了更深入地理解 AsyncLocal 我们可以查看一下源码,看看内部实现原理。

internal static readonly ExecutionContext Default = new ExecutionContext();
private static volatile ExecutionContext? s_defaultFlowSuppressed; private readonly IAsyncLocalValueMap? m_localValues;
private readonly IAsyncLocal[]? m_localChangeNotifications;
private readonly bool m_isFlowSuppressed;
private readonly bool m_isDefault; private ExecutionContext()
{
m_isDefault = true;
} private ExecutionContext(
IAsyncLocalValueMap localValues,
IAsyncLocal[]? localChangeNotifications,
bool isFlowSuppressed)
{
m_localValues = localValues;
m_localChangeNotifications = localChangeNotifications;
m_isFlowSuppressed = isFlowSuppressed;
} public void GetObjectData(SerializationInfo info, StreamingContext context)
{
throw new PlatformNotSupportedException();
} public static ExecutionContext? Capture()
{
ExecutionContext? executionContext = Thread.CurrentThread._executionContext;
if (executionContext == null)
{
executionContext = Default;
}
else if (executionContext.m_isFlowSuppressed)
{
executionContext = null;
} return executionContext;
} internal static object? GetLocalValue(IAsyncLocal local)
{
ExecutionContext? current = Thread.CurrentThread._executionContext;
if (current == null)
{
return null;
} Debug.Assert(!current.IsDefault);
Debug.Assert(current.m_localValues != null, "Only the default context should have null, and we shouldn't be here on the default context");
current.m_localValues.TryGetValue(local, out object? value);
return value;
} internal static void SetLocalValue(IAsyncLocal local, object? newValue, bool needChangeNotifications)
{
ExecutionContext? current = Thread.CurrentThread._executionContext; object? previousValue = null;
bool hadPreviousValue = false;
if (current != null)
{
Debug.Assert(!current.IsDefault);
Debug.Assert(current.m_localValues != null, "Only the default context should have null, and we shouldn't be here on the default context"); hadPreviousValue = current.m_localValues.TryGetValue(local, out previousValue);
} if (previousValue == newValue)
{
return;
} // Regarding 'treatNullValueAsNonexistent: !needChangeNotifications' below:
// - When change notifications are not necessary for this IAsyncLocal, there is no observable difference between
// storing a null value and removing the IAsyncLocal from 'm_localValues'
// - When change notifications are necessary for this IAsyncLocal, the IAsyncLocal's absence in 'm_localValues'
// indicates that this is the first value change for the IAsyncLocal and it needs to be registered for change
// notifications. So in this case, a null value must be stored in 'm_localValues' to indicate that the IAsyncLocal
// is already registered for change notifications.
IAsyncLocal[]? newChangeNotifications = null;
IAsyncLocalValueMap newValues;
bool isFlowSuppressed = false;
if (current != null)
{
Debug.Assert(!current.IsDefault);
Debug.Assert(current.m_localValues != null, "Only the default context should have null, and we shouldn't be here on the default context"); isFlowSuppressed = current.m_isFlowSuppressed;
newValues = current.m_localValues.Set(local, newValue, treatNullValueAsNonexistent: !needChangeNotifications);
newChangeNotifications = current.m_localChangeNotifications;
}
else
{
// First AsyncLocal
newValues = AsyncLocalValueMap.Create(local, newValue, treatNullValueAsNonexistent: !needChangeNotifications);
} //
// Either copy the change notification array, or create a new one, depending on whether we need to add a new item.
//
if (needChangeNotifications)
{
if (hadPreviousValue)
{
Debug.Assert(newChangeNotifications != null);
Debug.Assert(Array.IndexOf(newChangeNotifications, local) >= 0);
}
else if (newChangeNotifications == null)
{
newChangeNotifications = new IAsyncLocal[1] { local };
}
else
{
int newNotificationIndex = newChangeNotifications.Length;
Array.Resize(ref newChangeNotifications, newNotificationIndex + 1);
newChangeNotifications[newNotificationIndex] = local;
}
} Thread.CurrentThread._executionContext =
(!isFlowSuppressed && AsyncLocalValueMap.IsEmpty(newValues)) ?
null : // No values, return to Default context
new ExecutionContext(newValues, newChangeNotifications, isFlowSuppressed); if (needChangeNotifications)
{
local.OnValueChanged(previousValue, newValue, contextChanged: false);
}
}

从上面可以看出,ExecutionContext.GetLocalValue 和GetLocalValue.SetLocalValue 都是通过对 m_localValues 字段进行操作的。

m_localValues 的类型是 IAsyncLocalValueMap ,IAsyncLocalValueMap 的实现 和 AsyncLocal.cs 在一起,感兴趣的可以进一步查看 IAsyncLocalValueMap 是如何创建,如何查找的。

可以看到,里面最重要的就是ExecutionContext 的流动,线程发生变化时ExecutionContext 会在前一个线程中被默认捕获,流向下一个线程,它所保存的数据也就随之流动。在所有会发生线程切换的地方,基础类库(BCL) 都为我们封装好了对执行上下文的捕获 (如开始的例子,可以看到 AsyncLocal 的数据不会随着线程的切换而丢失),这也是为什么 AsyncLocal 能实现 线程切换后,还能正常获取数据,不丢失。

总结

  1. AsyncLocal 本身不保存数据,数据保存在 ExecutionContext 实例。

  2. ExecutionContext 的实例会随着线程切换流向下一线程(也可以禁止流动和恢复流动),保证了线程切换时,数据能正常访问。

在.NET Core 中的使用示例

  1. 先创建一个上下文对象
点击查看代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks; namespace NetAsyncLocalExamples.Context
{
/// <summary>
/// 请求上下文 租户ID
/// </summary>
public class RequestContext
{
/// <summary>
/// 获取请求上下文
/// </summary>
public static RequestContext Current => _asyncLocal.Value;
private readonly static AsyncLocal<RequestContext> _asyncLocal = new AsyncLocal<RequestContext>(); /// <summary>
/// 将请求上下文设置到线程全局区域
/// </summary>
/// <param name="userContext"></param>
public static IDisposable SetContext(RequestContext userContext)
{
_asyncLocal.Value = userContext;
return new RequestContextDisposable();
} /// <summary>
/// 清除上下文
/// </summary>
public static void ClearContext()
{
_asyncLocal.Value = null;
} /// <summary>
/// 租户ID
/// </summary>
public string TenantId { get; set; } }
} namespace NetAsyncLocalExamples.Context
{
/// <summary>
/// 用于释放对象
/// </summary>
internal class RequestContextDisposable : IDisposable
{
internal RequestContextDisposable() { }
public void Dispose()
{
RequestContext.ClearContext();
}
}
}
  1. 创建请求上下文中间件
点击查看代码
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using NetAsyncLocalExamples.Context;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; namespace NetAsyncLocalExamples.Middlewares
{
/// <summary>
/// 请求上下文
/// </summary>
public class RequestContextMiddleware : IMiddleware
{ protected readonly IServiceProvider ServiceProvider;
private readonly ILogger<RequestContextMiddleware> Logger;
public RequestContextMiddleware(IServiceProvider serviceProvider, ILogger<RequestContextMiddleware> logger)
{ ServiceProvider = serviceProvider;
Logger = logger;
}
public virtual async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var requestContext = new RequestContext();
using (RequestContext.SetContext(requestContext))
{
requestContext.TenantId = $"租户ID:{DateTime.Now.ToString("yyyyMMddHHmmsss")}";
await next(context);
}
} }
}
  1. 注册中间件
点击查看代码
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<RequestContextMiddleware>();
services.AddRazorPages();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
} app.UseHttpsRedirection();
app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); //增加上下文
app.UseMiddleware<RequestContextMiddleware>(); app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
}
  1. 一次赋值,到处使用
点击查看代码
namespace NetAsyncLocalExamples.Pages
{
public class IndexModel : PageModel
{
private readonly ILogger<IndexModel> _logger; public IndexModel(ILogger<IndexModel> logger)
{
_logger = logger;
_logger.LogInformation($"测试获取全局变量1:{RequestContext.Current.TenantId}");
} public void OnGet()
{
_logger.LogInformation($"测试获取全局变量2:{RequestContext.Current.TenantId}");
}
}
}

谈谈.NET Core下如何利用 AsyncLocal 实现共享变量的更多相关文章

  1. Asp.net core下利用EF core实现从数据实现多租户(1)

    前言 随着互联网的的高速发展,大多数的公司由于一开始使用的传统的硬件/软件架构,导致在业务不断发展的同时,系统也逐渐地逼近传统结构的极限. 于是,系统也急需进行结构上的升级换代. 在服务端,系统的I/ ...

  2. Asp.net core下利用EF core实现从数据实现多租户(3): 按Schema分离 附加:EF Migration 操作

    前言 前段时间写了EF core实现多租户的文章,实现了根据数据库,数据表进行多租户数据隔离. 今天开始写按照Schema分离的文章. 其实还有一种,是通过在数据表内添加一个字段做多租户的,但是这种模 ...

  3. .NET Core下使用gRpc公开服务(SSL/TLS)

    一.前言 前一阵子关于.NET的各大公众号都发表了关于gRpc的消息,而随之而来的就是一波关于.NET Core下如何使用的教程,但是在这众多的教程中基本都是泛泛而谈,难以实际在实际环境中使用,而该篇 ...

  4. .Net Core下如何管理配置文件

    一.前言 根据该issues来看,System.Configuration在.net core中已经不存在了,那么取而代之的是由Microsoft.Extensions.Cnfiguration.XX ...

  5. .Net Core下如何管理配置文件(转载)

    原文地址:http://www.cnblogs.com/yaozhenfa/p/5408009.html 一.前言 根据该issues来看,System.Configuration在.net core ...

  6. Asp.Net Core 轻松学-利用文件监视进行快速测试开发

    前言     在进行 Asp.Net Core 应用程序开发过程中,通常的做法是先把业务代码开发完成,然后建立单元测试,最后进入本地系统集成测试:在这个过程中,程序员的大部分时间几乎都花费在开发.运行 ...

  7. Asp.Net Core下的两种路由配置方式

    与Asp.Net Mvc创建区域的时候会自动为你创建区域路由方式不同的是,Asp.Net Core下需要自己手动做一些配置,但更灵活了. 我们先创建一个区域,如下图 然后我们启动访问/Manage/H ...

  8. .NET CORE下最快比较两个文件内容是否相同的方法 - 续

    .NET CORE下最快比较两个文件内容是否相同的方法 - 续 在上一篇博文中, 我使用了几种方法试图找到哪个是.NET CORE下最快比较两个文件的方法.文章发布后,引起了很多博友的讨论, 在此我对 ...

  9. 4.5 .net core下直接执行SQL语句并生成DataTable

    .net core可以执行SQL语句,但是只能生成强类型的返回结果.例如var blogs = context.Blogs.FromSql("SELECT * FROM dbo.Blogs& ...

随机推荐

  1. k8s学习笔记一(搭建&部署helloworld应用)

    kubernetes 目录 kubernetes 虚拟机创建三个节点 k8s install 部署hello world 应用 issue 汇总 node 一直处理NotReady状态 重启系统后虚拟 ...

  2. 6月29日学习总结 Django自带的用户认证

    Django自带的用户认证 我们在开发一个网站的时候,无可避免的要设计.实现网站的用户系统.此时我们需要实现包括但不限于用户注册.用户登录.用户认证.注销.修改密码等功能,这还真是个麻烦的事情呢. D ...

  3. Ubuntu16.04 搭建samba服务器

    1昨天花了一天时间弄了NFS服务器,结果搭建完之后出现各种问题,要么挂载不上,要么就是字符乱码.今天在看到一个关于树莓派的介绍的时候,提到Samba服务器的搭建,我尝试了一下,结果发现很顺利地就能够正 ...

  4. 什么是 Hystrix 断路器?我们需要它吗?

    由于某些原因,employee-consumer 公开服务会引发异常.在这种情况下使用Hystrix 我们定义了一个回退方法.如果在公开服务中发生异常,则回退方法返回一些默认值. 如果 firstPag ...

  5. Java LRU实现方式

    动画理解LRU算法:https://www.pianshen.com/article/8150146075/ Java实现LRU算法:https://www.pianshen.com/article/ ...

  6. Spring与Web项目整合的原理

    引言: 在刚开始我们接触IOC时,我们加载并启用SpringIOC是通过如下代码手动加载 applicationContext.xml 文件,new出context对象,完成Bean的创建和属性的注入 ...

  7. Python学习--21天Python基础学习之旅(Day05、Day06、Day07)

    Day05: Chapter 8 函数 1.1函数定义与调用 1.1.1向函数传递参数 1.2传递实参 1.2.1位置实参:基于实参顺序 1.2.2关键字实参:调用时指出各个实参对应的形参 1.2.3 ...

  8. 基于HTML5的拓扑图编辑器(2)

    继续来说编辑器的需求, 前面介绍了拖拽创建节点.以及连线的方法,并加入到了其后的 Qunee 类库,实际应用中需要更多功能,Qunee 的拓扑图编辑器也在逐渐完善,一方面增加多种编辑交互,一方面提供数 ...

  9. 快如闪电,触控优先。新一代的纯前端控件集 WijmoJS发布新版本了

    全球最大的控件提供商葡萄城宣布,新一代纯前端控件 WijmoJS 发布2018 v1 版本,进一步增强产品功能,并支持在 Npm 上的安装和发布,极大的提升了产品的易用性. WijmoJS 是用 Ty ...

  10. java中什么叫覆盖Override?请给实例

    5.覆盖(Override) 马克-to-win:方法的覆盖(Override)是指子类重写从父类继承来的一个同名方法(参数.返回值也同). 例1.5.1-- class AAAMark_to_win ...