.NET?Core利用?AsyncLocal?實(shí)現(xiàn)共享變量的代碼詳解
簡介
我們?nèi)绻枰麄€程序共享一個變量,我們僅需將該變量放在某個靜態(tài)類的靜態(tài)變量上即可(不滿足我們的需求,靜態(tài)變量上,整個程序都是固定值)。我們在Web 應(yīng)用程序中,每個Web 請求服務(wù)器都為其分配了一個獨(dú)立線程,如何實(shí)現(xiàn)用戶,租戶等信息隔離在這些獨(dú)立線程中。這就是今天要說的線程本地存儲。針對線程本地存儲 .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 都能實(shí)現(xiàn)基于線程的本地存儲。但是當(dāng)線程切換后,只有 AsyncLocal 還能夠保留原來的值。在Web 開發(fā)中,我們會有很多異步場景,在這些場景下,可能會出現(xiàn)線程的切換。所以我們使用AsyncLocal 去實(shí)現(xiàn)在Web 應(yīng)用程序下的共享變量。
AsyncLocal 解讀
源碼查看:
public sealed class AsyncLocal<T> : IAsyncLocal { private readonly Action<AsyncLocalValueChangedArgs<T>>? m_valueChangedHandler; // // 無參構(gòu)造函數(shù) // public AsyncLocal() { } // // 構(gòu)造一個帶有委托的AsyncLocal<T>,該委托在當(dāng)前值更改時被調(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 屬性進(jìn)行訪問 或者通過 ExecutionContext.Capture() 獲取。
從上面我們可以看出 AsyncLocal 的 Value 存取是通過 ExecutionContext.GetLocalValue 和GetLocalValue.SetLocalValue 進(jìn)行操作的,我們可以繼續(xù)從 ExecutionContext 里面取出部分代碼查看(源碼地址),為了更深入地理解 AsyncLocal 我們可以查看一下源碼,看看內(nèi)部實(shí)現(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 字段進(jìn)行操作的。
m_localValues 的類型是 IAsyncLocalValueMap ,IAsyncLocalValueMap 的實(shí)現(xiàn) 和 AsyncLocal.cs 在一起,感興趣的可以進(jìn)一步查看 IAsyncLocalValueMap 是如何創(chuàng)建,如何查找的。
可以看到,里面最重要的就是ExecutionContext 的流動,線程發(fā)生變化時ExecutionContext 會在前一個線程中被默認(rèn)捕獲,流向下一個線程,它所保存的數(shù)據(jù)也就隨之流動。在所有會發(fā)生線程切換的地方,基礎(chǔ)類庫(BCL) 都為我們封裝好了對執(zhí)行上下文的捕獲 (如開始的例子,可以看到 AsyncLocal 的數(shù)據(jù)不會隨著線程的切換而丟失),這也是為什么 AsyncLocal 能實(shí)現(xiàn) 線程切換后,還能正常獲取數(shù)據(jù),不丟失。
總結(jié)
AsyncLocal 本身不保存數(shù)據(jù),數(shù)據(jù)保存在 ExecutionContext 實(shí)例。
ExecutionContext 的實(shí)例會隨著線程切換流向下一線程(也可以禁止流動和恢復(fù)流動),保證了線程切換時,數(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> /// 將請求上下文設(shè)置到線程全局區(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 實(shí)現(xiàn)共享變量的代碼詳解的文章就介紹到這了,更多相關(guān).NET Core共享變量內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
ASP.NET MVC HtmlHelper如何擴(kuò)展
ASP.NET MVC 中HtmlHelper方法為我們提供很多html標(biāo)簽,只需在頁面調(diào)用就行了,但是微軟并沒有把所有的html標(biāo)簽都對應(yīng)有了擴(kuò)展方法,需要我們自定義HtmlHelper,來滿足我們需要。2016-05-05C#設(shè)置本地網(wǎng)絡(luò)如DNS、網(wǎng)關(guān)、子網(wǎng)掩碼、IP等等
手動設(shè)置本地網(wǎng)絡(luò)的方法顯然很不可取,所以我們要讓程序幫我們完成,需要的朋友可以參考下2014-03-03asp.net獲取ListView與gridview中當(dāng)前行的行號
這篇文章主要介紹了asp.net獲取ListView與gridview中當(dāng)前行的行號,實(shí)例分析了asp.net針對ListView與gridview獲取行號的實(shí)現(xiàn)技巧,需要的朋友可以參考下2016-01-01ASP.NET Core 2.2中的Endpoint路由詳解
這篇文章主要介紹了ASP.NET Core 2.2中的Endpoint路由詳解,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-03-03Asp.net core利用MediatR進(jìn)程內(nèi)發(fā)布/訂閱詳解
這篇文章主要給大家介紹了關(guān)于Asp.net core利用MediatR進(jìn)程內(nèi)發(fā)布/訂閱的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用Asp.net core具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06HTML服務(wù)器控件和WEB服務(wù)器控件的區(qū)別和聯(lián)系介紹
學(xué)習(xí)asp.net的時候一會用Html服務(wù)器控件,一會用Web服務(wù)器控件,起初做起例子來也挺迷糊的,下面對這兩個控件研究了一下做個筆記在此與大家分享下,感興趣的朋友可以了解下2013-08-08在應(yīng)用程序級別之外使用注冊為allowDefinition=''MachineToApplication''的節(jié)是錯誤的
在應(yīng)用程序級別之外使用注冊為 allowDefinition='MachineToApplication' 的節(jié)是錯誤的2009-03-03.Net?core?Blazor+自定義日志提供器實(shí)現(xiàn)實(shí)時日志查看器的原理解析
我們經(jīng)常遠(yuǎn)程連接服務(wù)器去查看日志,比較麻煩,如果直接訪問項(xiàng)目的某個頁面就能實(shí)時查看日志就比較奈斯了,結(jié)合blazor實(shí)現(xiàn)了基本效果,這篇文章主要介紹了.Net?core?Blazor+自定義日志提供器實(shí)現(xiàn)實(shí)時日志查看器,需要的朋友可以參考下2022-10-10