.NET?Core利用?AsyncLocal?實現(xiàn)共享變量的代碼詳解
簡介
我們?nèi)绻枰麄€程序共享一個變量,我們僅需將該變量放在某個靜態(tài)類的靜態(tài)變量上即可(不滿足我們的需求,靜態(tài)變量上,整個程序都是固定值)。我們在Web 應用程序中,每個Web 請求服務器都為其分配了一個獨立線程,如何實現(xiàn)用戶,租戶等信息隔離在這些獨立線程中。這就是今天要說的線程本地存儲。針對線程本地存儲 .NET 給我們提供了兩個類 ThreadLocal 和 AsyncLocal。我們可以通過查看以下例子清晰的看到兩者的區(qū)別:
[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}");
}
}
輸出結(jié)果:
CurrThread:threadId:4;threadLocal:threadLocal
StartNew:threadId:11; threadLocal:
CurrThread:threadId:4;asyncLocal:asyncLocal
StartNew:threadId:11; asyncLocal:asyncLocal
從上面結(jié)果中可以看出 ThreadLocal 和 AsyncLocal 都能實現(xiàn)基于線程的本地存儲。但是當線程切換后,只有 AsyncLocal 還能夠保留原來的值。在Web 開發(fā)中,我們會有很多異步場景,在這些場景下,可能會出現(xiàn)線程的切換。所以我們使用AsyncLocal 去實現(xiàn)在Web 應用程序下的共享變量。
AsyncLocal 解讀
源碼查看:
public sealed class AsyncLocal<T> : IAsyncLocal
{
private readonly Action<AsyncLocalValueChangedArgs<T>>? m_valueChangedHandler;
//
// 無參構(gòu)造函數(shù)
//
public AsyncLocal()
{
}
//
// 構(gòu)造一個帶有委托的AsyncLocal<T>,該委托在當前值更改時被調(diào)用
// 在任何線程上
//
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中的非泛型代碼調(diào)用泛型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 里面,每個線程都關(guān)聯(lián)著執(zhí)行上下文。我們可以通 Thread.CurrentThread.ExecutionContext 屬性進行訪問 或者通過 ExecutionContext.Capture() 獲取。
從上面我們可以看出 AsyncLocal 的 Value 存取是通過 ExecutionContext.GetLocalValue 和GetLocalValue.SetLocalValue 進行操作的,我們可以繼續(xù)從 ExecutionContext 里面取出部分代碼查看(源碼地址),為了更深入地理解 AsyncLocal 我們可以查看一下源碼,看看內(nèi)部實現(xiàn)原理。
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 的實現(xiàn) 和 AsyncLocal.cs 在一起,感興趣的可以進一步查看 IAsyncLocalValueMap 是如何創(chuàng)建,如何查找的。
可以看到,里面最重要的就是ExecutionContext 的流動,線程發(fā)生變化時ExecutionContext 會在前一個線程中被默認捕獲,流向下一個線程,它所保存的數(shù)據(jù)也就隨之流動。在所有會發(fā)生線程切換的地方,基礎(chǔ)類庫(BCL) 都為我們封裝好了對執(zhí)行上下文的捕獲 (如開始的例子,可以看到 AsyncLocal 的數(shù)據(jù)不會隨著線程的切換而丟失),這也是為什么 AsyncLocal 能實現(xiàn) 線程切換后,還能正常獲取數(shù)據(jù),不丟失。
總結(jié)
AsyncLocal 本身不保存數(shù)據(jù),數(shù)據(jù)保存在 ExecutionContext 實例。
ExecutionContext 的實例會隨著線程切換流向下一線程(也可以禁止流動和恢復流動),保證了線程切換時,數(shù)據(jù)能正常訪問。
1.在.NET Core 中的使用示例先創(chuàng)建一個上下文對象
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>
/// 將請求上下文設置到線程全局區(qū)域
/// </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.創(chuàng)建請求上下文中間件
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}");
}
}
}
到此這篇關(guān)于.NET Core利用 AsyncLocal 實現(xiàn)共享變量的代碼詳解的文章就介紹到這了,更多相關(guān).NET Core共享變量內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
在 .NET Core 中使用 Diagnostics (Diagnostic Source) 記錄跟蹤信息
今天給大家講一下在 .NET Core 2 中引入的全新 DiagnosticSource 事件機制,為什么說是全新呢? 在以前的 .NET Framework 有心的同學應該知道也有 Diagnostics,那么新的 .NET Core 中有什么變化呢?跟隨小編一起看看吧2021-06-06
asp.net中上傳圖片文件實現(xiàn)防偽圖片水印并寫入數(shù)據(jù)庫
asp.net上傳圖片文件實現(xiàn)防偽圖片水印并寫入數(shù)據(jù)庫,需要的朋友可以參考下。2010-10-10
ASP.NET Core使用GraphQL第一章之Hello World
這篇文章主要給大家介紹了關(guān)于ASP.NET Core使用GraphQL第一章之Hello World的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2018-11-11
Asp.NET MVC中使用SignalR實現(xiàn)推送功能
這篇文章主要為大家詳細介紹了Asp.NET MVC 中使用 SignalR 實現(xiàn)推送功能,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-10-10
URLRewriter最簡單入門介紹 URLRewriter相關(guān)資源
配置好后,查看日志看到的狀態(tài)都是200,IIS直接認為這個文件是存在的了, 而不是301,或302,這在某些情況下可能會不適用,比如:搜索引擎優(yōu)化時目錄或文件調(diào)整。2008-07-07
如何在ASP.Net Core使用分布式緩存的實現(xiàn)
這篇文章主要介紹了如何在ASP.Net Core使用分布式緩存的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-02-02

