談談.NET Core下如何利用 AsyncLocal 實現共用變數

来源:https://www.cnblogs.com/Hnj-Koala/archive/2022/04/13/16141626.html
-Advertisement-
Play Games

前言 在Web 應用程式中,我們經常會遇到這樣的場景,如用戶信息,租戶信息本次的請求過程中都是固定的,我們希望是這種信息在本次請求內,一次賦值,到處使用。本文就來探討一下,如何在.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<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}");
        }
    }
}

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前言 水果忍者到家都玩過吧,但是Python寫的水果忍者你肯定沒有玩過。今天就給你表演一個新的,用Python寫一個水果忍者。 水果忍者的玩法很簡單,儘可能的切開拋出的水果就行。 今天就用python簡單的模擬一下這個游戲。在這個簡單的項目中,我們用滑鼠選擇水果來切割,同時炸彈也會隱藏在水果 中,如 ...
  • 一、RedisInsight 簡介 RedisInsight 是一個直觀高效的 Redis GUI 管理工具,它可以對 Redis 的記憶體、連接數、命中率以及正常運行時間進行監控,並且可以在界面上使用 CLI 和連接的 Redis 進行交互(RedisInsight 內置對 Redis 模塊支持): ...
  • 前言 風玫瑰是由氣象學家用於給出如何風速和風向在特定位置通常分佈的簡明視圖的圖形工具。它也可以用來描述空氣質量污染源。 風玫瑰工具使用Matplotlib作為後端。 安裝方式直接使用pip install windrose 導入模塊 Python學習交流Q群:906715085#### import ...
  • 你好呀,我是歪歪。 我最近在 stackoverflow 上看到一段代碼,怎麼說呢。 就是初看一臉懵逼,看懂直接跪下! 我先帶你看看 stackoverflow 上的這個問題是啥,然後引出這段代碼: https://stackoverflow.com/questions/15182496/why-d ...
  • HashMap是大廠java語言的常考點,主要從底層結構,和線程安全等角度來進行考察,考察點比較集中,但是有一定難度 ...
  • 11月8日Spring官方已經強烈建議使用Spring Authorization Server替換已經過時的Spring Security OAuth2.0,距離Spring Security OAuth2.0結束生命周期還有小半年的時間,是時候做出改變了。目前Spring Authorizati ...
  • 本文主要解決兩個問題 * C# Winform高DPI字體模糊. * 高DPI下(縮放>100%), UI設計器一直提示縮放到100%, 如果不重啟到100%,設計的控制項會亂飛. ...
  • 1、導航查詢特點 作用:主要處理主對象裡面有子對象這種層級關係查詢 1.1 無外鍵開箱就用 其它ORM導航查詢 需要 各種配置或者外鍵,而SqlSugar則開箱就用,無外鍵,只需配置特性和主鍵就能使用 1.2 高性能優 查詢 性能非常強悍 支持大數據分頁導航查詢 3.3 語法超級爽 註意:多級查詢時 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...