前言:
自從使用了 AsyncLocal 後,就發現 AsyncLocal 變量像個臭蟲一樣,在有 AsyncLocal 變量的線程中啓動的 Task 、或者 Thread 都會附帶 AsyncLocal 變量。
在項目使用 AsyncLocal 實現了全局、局部 工作單元 ,但是就無法在後續作業中開啓多個線程了(需求就是要開啓多個線程,俺也沒得辦法),後續啓動的多線程都會帶有 AsyncLocal 變量,直接導致報錯,例如 DBContext 不是線程安全的錯之類的....。
其實我一直認爲在一個Http請求中開啓多個線程,不合適,應該把需要執行的任務交給 “後臺工作線程” ,或者交給 “後臺Job” ,但現實世界中的情況就是很複雜,怎麼辦?就是要在Http請求中開啓多個線程,還能怎麼辦呢,去解決 ExecutionContext 、AsyncLocal 傳遞的問題吧。
“人天天都學到一點東西,而往往所學到的是發現昨日學到的是錯的。”
Thread 中的 ExecutionContext
創建一個線程,並啓動,Thread執行的委託中會取到 “AsyncLocalTest.Lang.Value” 在線程外部設置的值。
爲啥Thread會取到外部的 AsyncLocal 變量中的值呢?深入源代碼看下,如下圖。
好傢伙,Thread.Start() 原來線程啓動時,就去執行ExecutionContext.Capture()獲取了線程執行上下文,即 ExecutionContext
如下圖,可以看到在Thread線程中可以獲取到 ExecutionContext ,從ExecutionContext中可以看到存儲在上面的 AsyncLocal 變量
Task中的ExecutionContext
聲明Task時,深入源代碼查看
Task 會再執行一個內部構造函數
Task 構造函數中,原來還是通過執行 ExecutionContext.Capture() 獲取了 ExecutionContext
創建一個Task時,Task就自動獲取了“線程執行上下文 即 ExecutionContext”。
阻止ExecutionContext流動
如何阻止ExecutionContext流動,請查看這篇文章 https://www.cnblogs.com/eventhorizon/p/12240767.html#3executioncontext-%E7%9A%84%E6%B5%81%E5%8A%A8 ,就不再贅述。
實現一個局部乾淨的ExecutionContext
1.實現一個 DisposeAction ,不知道怎麼稱呼,請看代碼吧,源代碼來只ABP框架,我直接copy過來的。原理,就是Using代碼塊釋放時,執行這個 “Action 委託”。
/// <summary> /// 源代碼來自ABP Vnext框架 /// </summary> public class DisposeAction : IDisposable { private readonly Action _action; public DisposeAction([NotNull] Action action) { _action = action ?? throw new ArgumentNullException(nameof(action)); } public void Dispose() { _action(); } }
2. 衆所周知 ExecutionContext.SuppressFlow() , 阻斷 ExecutionContext 流動 。ExecutionContext.RestoreFlow(), 啓動 ExecutionContext 流動 。
3. 實現局部阻斷 ExecutionContext 流動核心代碼
public class SuppressExecutionContextFlow { public static IDisposable CleanEnvironment() { // 阻斷 ExecutionContext 流動 ExecutionContext.SuppressFlow(); return new DisposeAction(() => { if (ExecutionContext.IsFlowSuppressed()) { ExecutionContext.RestoreFlow(); } }); } }
4.測試代碼,隨便調試下
//6.創建一個乾淨的 ExecutionContext 環境,供使用 var scheduler = new QueuedTaskScheduler(2); AsyncLocalTest.Lang.Value = "test"; using (SuppressExecutionContextFlow.CleanEnvironment()) { Task task11 = new Task(() => { var aa = ExecutionContext.Capture(); Console.WriteLine("task11線程:" + AsyncLocalTest.Lang.Value); }); Thread th = new Thread(() => { var aa = ExecutionContext.Capture(); Console.WriteLine("th線程:" + AsyncLocalTest.Lang.Value); }); th.Start(); task11.Start(scheduler); } Console.WriteLine("主線程:" + AsyncLocalTest.Lang.Value); Console.Read();
調試.gif
自此 實現一個局部乾淨的ExecutionContext 完成,我的代碼參考 https://github.com/qiqiqiyaya/Learning-Case/tree/main/CleanExecutionContext