浅谈AsyncLocal,我们应该知道的那些事儿
【導讀】最近查看有關框架源碼,發現AsyncLocal這玩意水還挺深,于是花了一點功夫去研究,同時對比ThreadLocal說明二者區別以及在何時場景下使用AsyncLocal或ThreadLocal
ThreadLocal相信很多童鞋用過,但AsyncLocal具體使用包括我在內的一大部分童鞋應該完全沒怎么使用過。
AsyncLocal同樣出現在.NET Framework 4.6+(包括4.6),當然在.NET Core中沒有版本限制即CoreCLR,對此類官方所給的解釋是:將本地環境數據傳遞到異步控制流,例如異步方法
又例如緩存WCF通信通道,可以使用AsyncLocal而不是.NET Framework或CoreCLR所提供的ThreadLocal
官方概念解釋在我們初次聽來好像還是有點抽象,不打緊,接下來我們通過實際例子來進行詳細說明和解釋
AsyncLocal和ThreadLocal區別
首先我們先看如下例子,然后再分析二者和什么有關系
private?static?readonly?ThreadLocal<string>?threadLocal?=?new?ThreadLocal<string>();private?static?readonly?AsyncLocal<string>?asyncLocal?=?new?AsyncLocal<string>();static?async?Task?Main(string[]?args) {threadLocal.Value?=?"threadLocal";asyncLocal.Value?=?"asyncLocal";await?Task.Yield();Console.WriteLine("After?await:?"?+?threadLocal.Value);Console.WriteLine("After?await:?"?+?asyncLocal.Value);Task.Run(()?=>?Console.WriteLine("Inside?child?task:?"?+?threadLocal.Value)).Wait();Task.Run(()?=>?Console.WriteLine("Inside?child?task:?"?+?asyncLocal.Value)).Wait();Console.ReadLine(); }猜猜如上將會打印出什么結果呢?
為何ThreadLocal所打印的值為空值呢?我們不是設置了值嗎?此時我們將要從執行環境開始說起
若完全理解ExecutionContext與SynchronizationContext二者概念和關系,理論上來講則可解答出上述問題,這里我們簡單敘述下,更詳細介紹請查閱相關資料自行了解
ExecutionContext俗稱“執行上下文”,也就是說和“環境”信息相關,這也就意味著它存儲著和我們當前程序所執行的環境相關的數據,這類環境信息數據存儲在ThreadStatic或ThreadLocal中,換句話說ThreadLocal和特定線程相關
上述我們討論的是相同環境或上下文中,若是不同上下文即不同線程中,那情況又該如何呢?
在異步操作中,在某一個線程中啟動操作,但卻在另一線程中完成,此時我們將不能利用ThreadLocal來存儲數據,因線程切換所需存儲數據,我們可以稱之為環境“流動”
對于邏輯控制流,我們期望的是執行環境相關數據能同控制流一起流動,以便能讓執行環境相關數據能從一個線程移動到另外一個線程,ExecutionContext的作用就在于此。而SynchronizationContext是一種抽象,比如Windows窗體則提供了WindowsFormSynchronizationContext上下文等等
SynchronizationContext作為ExecutionContext執行環境的一部分
ExecutionContext是當前執行環境,而SynchronizationContext則是針對不同框架或UI的抽象
我們可通過SynchronizationContext.Current得到當前執行環境信息。
到這里想必我們已經明白基于特定線程的ThreadLocal在當前線程設置值后,但await卻不在當前線程,所以打印值為空,若將上述第一個await去除,則可打印出設置值,而AsyncLocal卻是和執行環境相關,也就是說與線程和調用堆棧有關,并不針對特定線程,它是流動的。
AsyncLocal原理初步分析
首先我們通過一個簡單的例子來演示AsyncLocal類中值變化過程,我們能從表面上可得出的結論,然后最終結合源碼進行進一步分析
private?static?readonly?AsyncLocal<string>?asyncLocal?=?new?AsyncLocal<string>();static?async?Task?Main(string[]?args) {asyncLocal.Value?=?"asyncLocal";Task.Run(()?=>{asyncLocal.Value?=?"inside?child?task?asyncLocal";Console.WriteLine($"Inside?child?task:?{asyncLocal.Value}");}).Wait();Console.WriteLine($"after?await:{asyncLocal.Value}");Console.ReadLine(); }由上打印我們可看出,在Task方法內部將其值進行了修改并打印出修改過后的結果,在Task結束后,最終打印的卻是初始值。
在Task方法內部修改其值,但在任務結束后仍為初始值,這是一種“寫時復制”行為,AsyncLocal內部做了兩步操作
進行AsyncLocal實例的拷貝副本,但這是淺復制行為而非深復制
在設置新的值之前完成復制操作
接下來我們再通過一個層層調用例子并深入分析
private?static?readonly?AsyncLocal<string>?asyncLocal?=?new?AsyncLocal<string>();static?async?Task?Main(string[]?args) {Demo1().GetAwaiter().GetResult();Console.ReadLine(); }static?async?Task?Demo1() {await?Demo2();Console.WriteLine($"inside the method of demo1:{asyncLocal.Value}"); }static?async?Task?Demo2() {SetValue();Console.WriteLine($"inside the method of demo2:{asyncLocal.Value}"); }static?void?SetValue() {asyncLocal.Value?=?"initial?value"; }我們看到此時在Demo1方法內部打印值為空,因為在Demo2方法內部并未使用異步,所以能打印出所設置的值,這說明如下問題
每次進行實際的aysnc/await后,都會啟動一個新的異步上下文,并且該上下文與父異步上下文完全隔離且獨立,換句話說,在異步方法內,可查詢自己所屬AsyncLocal<T>,以便能確保不會污染父異步上下文,因為所做更改完全是針對當前異步上下文的本地內容
至于為何在Demo1方法內部打印為空,想必我們已經很清晰,當async方法返回時,返回的是父異步上下文,此時將看不到任何子異步上下文所執行的修改。
AsyncLocal原理源碼分析
我們來到AsyncLocal類,通過屬性Value設置值,內部通過調用ExecutionContext類中的SetLocalValue方法進行設置,源碼如下:
internal?static?void?SetLocalValue(IAsyncLocal?local,?object??newValue,?bool?needChangeNotifications) {ExecutionContext??current?=?Thread.CurrentThread._executionContext;object??previousValue?=?null;bool?hadPreviousValue?=?false;if?(current?!=?null){hadPreviousValue?=?current.m_localValues.TryGetValue(local,?out?previousValue);}if?(previousValue?==?newValue){return;}IAsyncLocal[]??newChangeNotifications?=?null;IAsyncLocalValueMap?newValues;bool?isFlowSuppressed?=?false;if?(current?!=?null){isFlowSuppressed?=?current.m_isFlowSuppressed;newValues?=?current.m_localValues.Set(local,?newValue,?treatNullValueAsNonexistent:?!needChangeNotifications);newChangeNotifications?=?current.m_localChangeNotifications;}else{newValues?=?AsyncLocalValueMap.Create(local,?newValue,?treatNullValueAsNonexistent:?!needChangeNotifications);}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?:?new?ExecutionContext(newValues,?newChangeNotifications,?isFlowSuppressed);if?(needChangeNotifications){local.OnValueChanged(previousValue,?newValue,?contextChanged:?false);} }當首次設置值時,我們通過Thread.CurrentThread.ExecutionContext,獲取其屬性將為空,通過AsyncLocalValueMap.Create創建一個AsyncLocal實例并設置值
同時我們也可以看到,若在同一執行環境中,當前最新設置值與之前所設置值相同,此時將不會是覆蓋,而是直接返回。
我們直接來到最后如下幾行代碼:
Thread.CurrentThread._executionContext?=(!isFlowSuppressed?&&?AsyncLocalValueMap.IsEmpty(newValues))??null?:?new?ExecutionContext(newValues,?newChangeNotifications,?isFlowSuppressed);若默認使用Task默認線程池調度,即使線程池重用線程,其執行環境上下文也會不同,如此可說明將更能保證不會將線程數據泄露到另外一個線程中,也就是說在重用線程時,但將會保證異步本地實例會按照預期進行GC(個人以為,理論上情況應該是這樣,這樣也能保證AsyncLocal是安全的)。
至于其他關于如何進行值更改后事件通知,這里就不再額外展開敘述
由于AsyncLocal使用淺拷貝,我們應保證存儲的數據類型不可變,若要修改AsyncLocal<T>實例值,必須保證異步上下文隔離且相互不會影響。
到這里我們已完全清楚,AsyncLocal是針對異步控制流的良好支持,且數據可流動,當前線程AsyncLocal實例所存儲的數據可流動到異步任務控制流中的默認任務調度線程池的線程中
當然我們也可以調用如下執行環境上下文中的抑制流動方法來禁用數據流動
private?static?readonly?AsyncLocal<string>?asyncLocal?=?new?AsyncLocal<string>();static?async?Task?Main(string[]?args) {asyncLocal.Value?=?"asyncLocal";using?(ExecutionContext.SuppressFlow()){Task.Run(()?=>{Console.WriteLine($"Inside?child?task:?{asyncLocal.Value}");}).Wait();}Console.WriteLine($"after?await:{asyncLocal.Value}");Console.ReadLine(); }此時在其任務內部打印的值將為空。最后,我們再來對AsyncLocal做一個最終總結
?????AsyncLocal出現于.NET Framework 4.6+(包含4.6)、CoreCLR
?????AsyncLocal是每個ExecutionContext實例的一個變量,它并非如同ThreadLocal基于特定線程的持久化數據存儲
?????若需要基于本地環境的異步控制流,使用AsyncLocal而非ThreadLocal,在線程池中重用線程時,ThreadLocal會保留之前值(基于理論猜測),而AsyncLocal不會
?????AsyncLocal在每次asyn/await后,都將重新生成一個新的異步執行上下文環境,父異步上下文執行環境和子異步上下文執行環境完全隔離且互不影響
?????AsyncLocal進行異步控制流時,由于內部對數據進行淺拷貝,確保其實例類型參數應為不可變數據類型
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的浅谈AsyncLocal,我们应该知道的那些事儿的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 移动建模平台元数据存储架构演进
- 下一篇: 如何在 Asp.Net Core MVC