agilelabs-fx-docs main topics/workcontext/task-run.md

Task.Run 与新线程中使用 WorkContext

本页说明 Task.RunTask.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 缓存到了长生命周期对象中。

相关页面