• 谈谈.NET Core下如何利用 AsyncLocal 实现共享变量


    前言

    在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>? m_valueChangedHandler;
    
        //
        // 无参构造函数
        //
        public AsyncLocal()
        {
        }
    
        //
        // 构造一个带有委托的AsyncLocal,该委托在当前值更改时被调用
        // 在任何线程上
        //
        public AsyncLocal(Action>? 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(previousValue, currentValue, contextChanged));
        }
    }
    
    //
    // 接口,允许ExecutionContext中的非泛型代码调用泛型AsyncLocal类型
    //
    internal interface IAsyncLocal
    {
        void OnValueChanged(object? previousValue, object? currentValue, bool contextChanged);
    }
    
    public readonly struct AsyncLocalValueChangedArgs
    {
        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
    {
        /// 
        /// 请求上下文  租户ID
        /// 
        public class RequestContext
        {
            /// 
            /// 获取请求上下文
            /// 
            public static RequestContext Current => _asyncLocal.Value;
            private readonly static AsyncLocal _asyncLocal = new AsyncLocal();
    
            /// 
            /// 将请求上下文设置到线程全局区域
            /// 
            /// 
            public static IDisposable SetContext(RequestContext userContext)
            {
                _asyncLocal.Value = userContext;
                return new RequestContextDisposable();
            }
    
            /// 
            /// 清除上下文
            /// 
            public static void ClearContext()
            {
                _asyncLocal.Value = null;
            }
    
            /// 
            /// 租户ID
            /// 
            public string TenantId { get; set; }
    
    
    
        }
    }
    
    namespace NetAsyncLocalExamples.Context
    {
        /// 
        /// 用于释放对象
        /// 
        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
    {
        /// 
        /// 请求上下文
        /// 
        public class RequestContextMiddleware : IMiddleware
        {
    
    
            protected readonly IServiceProvider ServiceProvider;
            private readonly ILogger Logger;
            public RequestContextMiddleware(IServiceProvider serviceProvider, ILogger 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();
    	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();
    
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapRazorPages();
        });
    }
    
    
    1. 一次赋值,到处使用
    点击查看代码
    namespace NetAsyncLocalExamples.Pages
    {
        public class IndexModel : PageModel
        {
            private readonly ILogger _logger;
    
            public IndexModel(ILogger logger)
            {
                _logger = logger;
                _logger.LogInformation($"测试获取全局变量1:{RequestContext.Current.TenantId}");
            }
    
            public void OnGet()
            {
                _logger.LogInformation($"测试获取全局变量2:{RequestContext.Current.TenantId}");
            }
        }
    }
    
  • 相关阅读:
    龙迅LT2611UX—LVDS至HDMI2.0转换器概述资料分享
    混淆矩阵和相应参数详解
    UE4/5中数组TArray
    「Spring」Boot Docker 认证指南(上)
    德克萨斯大学奥斯汀分校自然语言处理硕士课程汉化版(第八周) - 现代大语言模型
    新手小白学JAVA 日期类Date SimpleDateFormat Calendar
    搜索技术【广度优先搜索】 - 嵌套广度优先搜索 【POJ No. 1475】 推箱子 Pushing Boxes
    JimuReport 积木报表 v1.6.4 稳定版本正式发布 — 开源免费的低代码报表
    Java--MybatisPlus入门;与Mybatis区别;简单使用(一)
    【AtomicReference、AtomicStampedReference】常规用法
  • 原文地址:https://www.cnblogs.com/Hnj-Koala/p/16141626.html