前言

在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. luoguP6620 [省选联考 2020 A 卷] 组合数问题(斯特林数)

    luoguP6620 [省选联考 2020 A 卷] 组合数问题(斯特林数) Luogu 题外话: LN切这题的人比切T1的多. 我都想到了组合意义乱搞也想到可能用斯特林数为啥还是没做出来... 我怕 ...

  2. python3 爬虫 Scrapy库学习1

    1生成项目:生成项目文件夹 scrapy startproject 项目名 2生成爬虫文件 scrapy genspider 爬虫名 指定域名 3进入items文件可以输入自己想要爬取的内容比如 te ...

  3. 文字图片在wps中清晰化方法

    在wps中双击图片出属性,然后再选择文字增强.选择对比增加即可.

  4. spring cloud 和dubbo区别?

    1.服务调用方式 dubbo是RPC springcloud Rest Api2.注册中心,dubbo 是zookeeper springcloud是eureka,也可以是zookeeper3.服务网 ...

  5. ThreadLocal是什么?使用场景有哪些?

    什么是ThreadLocal? ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本. 测试代码: package ...

  6. Python学习--21天Python基础学习之旅(Day01、Day02)

    21天的python基础学习,使用<Python从入门到实践>,并且需要手敲书中的code,以下为整个学习过程的记录. Day01: 安装python时要选择复选框 Add Python ...

  7. C语言对源程序处理的四个步骤:预处理、编译、汇编、链接——预处理篇

    预处理 1)预处理的基本概念 C语言对源程序处理的四个步骤:预处理.编译.汇编.链接. 预处理是在程序源代码被编译之前,由预处理器(Preprocessor)对程序源代码进行的处理.这个过程并不对程序 ...

  8. pandas学习总结

    什么是pandas pandas数据读取 03. Pandas数据结构 Pandas查询数据的几种方法

  9. translate3d 对 z-index 居然有影响

    在 Mobile 端需要注意. 安卓 默认浏览器 当中如果 div1 div2 如果 div1 有 translate3d 而 div2 没有 那么 div2 的 z-index 会无效. 解决办法: ...

  10. CSS3 用border写 空心三角箭头 (两种写法)

    之前一直在寻找这种空心三角箭头, 终于知道了原理! 自己记录一下,顺便分享给之前跟我一样想要的撸友们~ 第一种写法 利用常见的 after伪元素 <!DOCTYPE html> <h ...