談談.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 解讀
原始碼檢視:
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 的例項會隨著執行緒切換流向下一執行緒(也可以禁止流動和恢復流動),保證了執行緒切換時,資料能正常訪問。
在.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();
}
}
}
- 建立請求上下文中介軟體
點選檢視程式碼
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);
}
}
}
}
- 註冊中介軟體
點選檢視程式碼
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}");
}
}
}