.NET Core利用 AsyncLocal 实现共享变量的代码详解

目录
  • 简介
  • 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 解读

官方文档

源码地址

源码查看:

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 能实现 线程切换后,还能正常获取数据,不丢失。

总结

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

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

1.在.NET Core 中的使用示例先创建一个上下文对象

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();
        }
    }
}

2.创建请求上下文中间件

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);
            }
        }
    }
}

3.注册中间件

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();
    });
}

一次赋值,到处使用

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 实现共享变量的代码详解的文章就介绍到这了,更多相关.NET Core共享变量内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 浅析C#中的AsnycLocal与ThreadLocal

    AsnyncLocal与ThreadLocal都是存储线程上下文的变量,但是,在实际使用过程中两者又有区别主要的表现在: AsyncLocal变量可以在父子线程中传递,创建子线程时父线程会将自己的AsyncLocal类型的上下文变量赋值到子线程中,但是,当子线程改变线程上下文中AsnycLocal变量值后,父线程不会同步改变.也就是说AsnycLocal变量只会影响他的子线程,不会影响他的父级线程. TreadLocal只是当前线程的上下文变量,不能在父子线程间同步. using System;

  • .NET core项目AsyncLocal在链路追踪中的应用

    目录 前言 老传统做法 AspNetCore的TraceIdentifier AsyncLocal在链路追踪的应用 定义 示例 项目应用 AspNet4 AspNetCore 前言 在项目生产中日志的记录是必不可少的,在.net项目中,要说日志组件,log4net绝对可有一席之地,随着公司业务的发展,微服务则必定无可避免.在跨服务中通过日志进行分析性能或者排查故障点,如何快速定位日志尤为关键.链路追踪技术的出现正是解决这些痛点的. 分布式链路追踪需要收集单次请求所经过的所有服务,而且为了知道请求

  • .NET Core分布式链路追踪框架的基本实现原理

    分布式追踪 什么是分布式追踪 分布式系统 当我们使用 Google 或者 百度搜索时,查询服务会将关键字分发到多台查询服务器,每台服务器在自己的索引范围内进行搜索,搜索引擎可以在短时间内获得大量准确的搜索结果:同时,根据关键字,广告子系统会推送合适的相关广告,还会从竞价排名子系统获得网站权重.通常一个搜索可能需要成千上万台服务器参与,需要经过许多不同的系统提供服务. 多台计算机通过网络组成了一个庞大的系统,这个系统即是分布式系统. 在微服务或者云原生开发中,一般认为分布式系统是通过各种中间件/服

  • ASP.Net Core中的日志与分布式链路追踪

    目录 .NET Core 中的日志 控制台输出 非侵入式日志 Microsoft.Extensions.Logging ILoggerFactory ILoggerProvider ILogger Logging Providers 怎么使用 日志等级 Trace.Debug 链路跟踪 OpenTracing 上下文和跟踪功能 跟踪单个功能 将多个跨度合并到一条轨迹中 传播过程中的上下文 分布式链路跟踪 在不同进程中跟踪 在 ASP.NET Core 中跟踪 OpenTracing API 和

  • .NET Core利用 AsyncLocal 实现共享变量的代码详解

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

  • Asp.net core利用MediatR进程内发布/订阅详解

    1.背景 最近,一个工作了一个月的同事离职了,所做的东西怼了过来.一看代码,惨不忍睹,一个方法六七百行,啥也不说了吧,实在没法儿说.介绍下业务场景吧,一个公共操作A,业务中各个地方都会做A操作,正常人正常思维应该是把A操作提取出来封装,其他地方调用,可这哥们儿偏偏不这么干,代码到处复制.仔细分析了整个业务之后,发现是一个典型的事件/消息驱动型,或者叫发布/订阅型的业务逻辑.鉴于系统是单体的,所以想到利用进程内发布/订阅的解决方案.记得很久之前,做WPF时候,用过Prism的EventAggreg

  • .net core利用orm如何操作mysql数据库详解

    前言 众所周知Mysql数据库由于其体积小.速度快.总体拥有成本低,尤其是开放源码这一特点,许多中小型网站为了降低网站总体拥有成本而选择了MySQL作为网站数据库.MySQL是一个多用户.多线程的关系型数据库管理系统. 工作模式是基于客户机/服务器结构.目前它可以支持几乎所有的操作系统. 简单的来说 ,MySql是一个开放的.快速的.多线程的.多用户的SQL数据库服务器. 下面讲解如何在.net core中使用mysql数据库,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. 操

  • Spring中利用配置文件和@value注入属性值代码详解

    1 简单属性值注入 package com.xy.test1; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @Service // 需要被注入属性值的类需要被Spring管理 public class PropertiesService1 { // 利用@Value注解,即使没有该属性或者属性文件也不会报错 // @Value输入

  • django在保存图像的同时压缩图像示例代码详解

    假设我们有一个非常简单的Post模型,它将是一个图像及其描述, from django.db import models class Post(models.Model): text = models.TextField() image = models.ImageField(upload_to='images/') 但是我们要优化图像大小,这将由我们Post的image字段指出. 这样做有充分的理由-它有助于更快地加载网站/应用程序并减少我们的服务器存储. 在使用Django之前,首先让我们简

  • 如何利用Ajax实现地区三级联动详解

    前言: 利用Ajax来实现一个地区的三级联动,用Java代码来读json文件,先eclipse做一个简单的,最基础的.(json我用的jackson来解析,也可用fastjson-阿里巴巴的等还有很多)提供代码,思路之类的,注释也没有自己去想去琢磨出来的思路好 first:首先先要熟悉json文件,并要想好利用什么类型去解析,这是最难的,最好找一个没人的地方戴上耳机(对于初学)我是用maven来做的用到的jar坐标 : <dependency> <groupId>redis.cli

  • SpringBoot利用AOP实现一个日志管理详解

    目录 1. 需求 2. 新建一张日志表 3. 写相应的Controller层 4.Service接口层 5.Service实现 6.Mapper接口 7.Mapper.xml(我用的是Mybatis) 8.CspLog 9.实体类SysOperCspLog 10. 定义日志管理的切面 11.AsyncFactoryCsp 12. 写一个Controller的Demo来执行一条日志试试 1. 需求 目前有这么个问题,有两个系统CSP和OMS,这俩系统共用的是同一套日志操作:Log;目前想区分下这俩

  • Angular4表单验证代码详解

     背景: 最近在itoo页面调整的时候,发现页面表单或者是文本框没有做基本的判断操作,所以着手demo一篇,希望对大家有帮助!! -------------------------------------------------------------------------------- 1.创建表单组件: ng g c login1 2.1单规则验证: <label>用户名:</label> <input type="text" #userNameRe

  • Java多线程之线程通信生产者消费者模式及等待唤醒机制代码详解

    前言 前面的例子都是多个线程在做相同的操作,比如4个线程都对共享数据做tickets–操作.大多情况下,程序中需要不同的线程做不同的事,比如一个线程对共享变量做tickets++操作,另一个线程对共享变量做tickets–操作,这就是大名鼎鼎的生产者和消费者模式. 正文 一,生产者-消费者模式也是多线程 生产者和消费者模式也是多线程的范例.所以其编程需要遵循多线程的规矩. 首先,既然是多线程,就必然要使用同步.上回说到,synchronized关键字在修饰函数的时候,使用的是"this"

  • spring boot application properties配置实例代码详解

    废话不多说了,直接给大家贴代码了,具体代码如下所示: # =================================================================== # COMMON SPRING BOOT PROPERTIES # # This sample file is provided as a guideline. Do NOT copy it in its # entirety to your own application. ^^^ # ========

随机推荐