前言:
自从使用了 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(); } }
DisposeAction
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(); } }); } }
SuppressExecutionContextFlow.CleanEnvironment
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();
干净的 ExecutionContext 环境
调试.gif
自此 实现一个局部干净的ExecutionContext 完成,我的代码参考 https://github.com/qiqiqiyaya/Learning-Case/tree/main/CleanExecutionContext