Task.Run 与新线程中使用 WorkContext
本页说明 Task.Run、Task.Factory.StartNew、线程池、新线程和 Parallel 场景中如何正确使用 WorkContext。核心原则是:新执行流必须创建新的 WorkContext Scope,不要复用父请求的 Scoped 服务。
场景
适用于:
Task.Run(...)Task.Factory.StartNew(...)ThreadPool.QueueUserWorkItem(...)new Thread(...)Parallel.For(...)- 请求内派生出的异步后台处理
- 非 HTTP 线程里需要解析 Scoped 服务的业务逻辑
如果是 ASP.NET Core 请求内同步业务,请看 ASP.NET Core 中使用 WorkContext。
正确入口
新执行流优先使用:
using var scope = workContext.CreateScopeWithWorkContextForNewTask(scopeName: "TaskName");
如果当前线程已经没有可用 workContext,使用应用级入口:
using var scope = AgileLabContexts.Context.CreateScopeWithWorkContextForNewTask();
同线程临时隔离才使用:
using var scope = workContext.CreateScopeWithWorkContext(scopeName: "ChildScope");
API 区别
| API | 用于 | Holder 行为 | Dispose 行为 |
|---|---|---|---|
CreateScopeWithWorkContext() |
同线程/同执行流子 Scope | 默认复用当前 Holder | 释放子上下文后恢复父 WorkContext |
CreateScopeWithWorkContextForNewTask() |
新 Task、新线程、线程池 | 强制创建新的 Holder | 释放当前上下文并清理独立 Holder |
AgileLabContexts.Context.CreateScopeWithWorkContextForNewTask() |
当前没有可用 WorkContext 的新执行边界 | 从 Root 创建新 Scope 和新 Holder | 释放独立上下文 |
为什么不能复用父请求服务
父请求中的 Repository、DbContext、业务 Service 都绑定父请求 Scope。把这些对象传进子线程会产生几个问题:
- 父请求结束后,这些对象可能已经 Dispose。
- 子线程和父线程同时使用同一个 Scoped 对象,线程安全不可控。
- 当前身份、租户、时区、审计字段可能串线。
- 数据库连接、事务、日志链路和释放顺序会失真。
正确做法是:只传递必要参数,在子线程内创建 WorkContext Scope,并从该 Scope 重新解析服务。
推荐模板:从请求中启动后台 Task
public Task ExportLaterAsync(string exportId)
{
_ = Task.Run(async () =>
{
using var scope = _workContext.CreateScopeWithWorkContextForNewTask(scopeName: "ExportOrders");
var exportService = scope.WorkContext.Resolve<IExportService>();
await exportService.ExportAsync(exportId);
});
return Task.CompletedTask;
}
推荐模板:没有父 WorkContext 的线程入口
_ = Task.Factory.StartNew(async () =>
{
using var scope = AgileLabContexts.Context.CreateScopeWithWorkContextForNewTask();
scope.WorkContext.SetName("StandaloneWorker");
var worker = scope.WorkContext.Resolve<IStandaloneWorker>();
await worker.RunAsync();
});
推荐模板:指定继承属性
如果子任务只需要继承身份、语言和时区,可以显式设置 inheritFlag:
var flags = PropertiesInheritFlag.Identity
| PropertiesInheritFlag.CultureInfo
| PropertiesInheritFlag.TimeZone;
_ = Task.Run(async () =>
{
using var scope = _workContext.CreateScopeWithWorkContextForNewTask(
inheritFlag: (uint)flags,
assignMode: PropertiesAssignMode.DeepClone,
scopeName: "SendNotification");
var service = scope.WorkContext.Resolve<INotificationService>();
await service.SendAsync(messageId);
});
DeepClone 可以减少子线程修改继承属性时影响父上下文的风险。Items 即使继承,也只是新建字典并保留字典内对象引用,不适合放非线程安全对象。
为什么不用 ExecutionContext.SuppressFlow
源码注释里明确说明:框架没有采用 ExecutionContext.SuppressFlow() 一刀切阻断上下文流动,因为它会同时阻断系统层上下文,例如 Thread Principal、Culture 等。
WorkContext 的实现选择在子线程中强制创建新的 WorkContextHolder。这样既保留系统上下文的自然流动,又隔离框架业务上下文,避免子线程修改父线程的 AsyncLocal Holder。
错误用法
public Task WrongAsync(string id)
{
var repository = _repository;
_ = Task.Run(async () =>
{
await repository.SaveAsync(id);
});
return Task.CompletedTask;
}
问题:
_repository是父请求 Scope 中解析出的 Scoped 服务。- 请求结束后它可能已经释放。
- 子线程没有自己的 WorkContext,EF Core 审计字段可能缺失或抛错。
改法:
public Task RightAsync(string id)
{
_ = Task.Run(async () =>
{
using var scope = _workContext.CreateScopeWithWorkContextForNewTask(scopeName: "SaveInBackground");
var repository = scope.WorkContext.Resolve<IOrderRepository>();
await repository.SaveAsync(id);
});
return Task.CompletedTask;
}
排障
子线程里 IWorkContextCore 为空
检查:
- 子线程里是否创建了
CreateScopeWithWorkContextForNewTask()。 - 是否从 RootServiceProvider 直接解析业务服务。
- 是否在 Scope Dispose 后继续使用服务。
子线程身份串线
检查:
- 是否误用了
CreateScopeWithWorkContext()。 - 是否继承了引用模式下的可变身份对象。
- 是否在子线程里修改了父上下文共享对象。
父请求结束后子任务报 ObjectDisposed
检查:
- 是否把父请求注入出来的服务传给子线程。
- 子 Scope 是否从 WorkContext 重新解析服务。
- 是否把
scope.WorkContext.ServiceProvider缓存到了长生命周期对象中。