Skip to content

DbContext threading

DbContext 应该被当成一次性、串行使用的工作单元,而不是可共享的并发容器。只要一段业务里存在并行执行,就要拆成多个 DbContext 实例,不要试图让同一个实例同时跑多个 EF Core 操作。

原文链接

我的结论

  • DbContext 的推荐生命周期很短,通常只覆盖一次 unit-of-work。
  • 在 ASP.NET Core 里,AddDbContext 默认按 scoped 注册;如果一个 HTTP 请求只做串行数据库操作,这个默认值通常就是正确的。
  • 真正危险的不是“异步”本身,而是同一个 DbContext 被多个未完成的操作共享,例如忘记 await、fire-and-forget、或者把同一个实例传进 Task.WhenAll(...)
  • 一旦业务明确需要并行数据库访问,就应该创建多个 DbContext,而不是给同一个实例“加锁硬撑”。

关键信息

1. DbContext 的设计前提是单次工作单元

一次典型的 EF Core 工作流通常是:

  1. 创建 DbContext
  2. 查询或附加实体并开始跟踪
  3. 修改实体
  4. 调用 SaveChanges / SaveChangesAsync
  5. 释放 DbContext

这意味着它不是长期缓存,也不是跨多个并发任务共享的状态容器。

2. Web 请求与 scoped 生命周期天然契合

AddDbContext 默认会把 DbContext 注册成 scoped,因此在常见 Web 应用里,一个请求对应一个 DbContext 实例 是比较自然的模型:

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));

如果控制器和服务都只在当前请求内串行访问数据库,这种模式既简单又安全。

3. EF Core 明确不支持同一个 DbContext 上的并行操作

下面这种写法是高风险的:

// ❌ 同一个 DbContext 上同时发起两个 EF Core 操作
Task<List<User>> usersTask = context.Users
    .Where(u => u.IsActive)
    .ToListAsync();

Task<List<Order>> ordersTask = context.Orders
    .Where(o => o.Status == OrderStatus.Pending)
    .ToListAsync();

await Task.WhenAll(usersTask, ordersTask);

问题不在 Task.WhenAll,而在于两个任务共享了同一个 context。EF Core 官方结论很直接:同一个 DbContext 实例不支持多条并行操作

4. 正确做法只有两类

串行执行:立即 await

List<User> users = await context.Users
    .Where(u => u.IsActive)
    .ToListAsync();

List<Order> orders = await context.Orders
    .Where(o => o.Status == OrderStatus.Pending)
    .ToListAsync();

并行执行:每个并行分支单独创建 DbContext

await using ApplicationDbContext usersContext =
    await factory.CreateDbContextAsync();
await using ApplicationDbContext ordersContext =
    await factory.CreateDbContextAsync();

Task<List<User>> usersTask = usersContext.Users
    .Where(u => u.IsActive)
    .ToListAsync();

Task<List<Order>> ordersTask = ordersContext.Orders
    .Where(o => o.Status == OrderStatus.Pending)
    .ToListAsync();

await Task.WhenAll(usersTask, ordersTask);

如果确实要并发访问数据库,核心原则不是“怎么让一个 DbContext 线程安全”,而是“不要共享它”。

摘录

A DbContext instance is designed to be used for a single unit-of-work. This means that the lifetime of a DbContext instance is usually very short.

In many web applications, each HTTP request corresponds to a single unit-of-work.

Entity Framework Core does not support multiple parallel operations being run on the same DbContext instance.

Therefore, always await async calls immediately, or use separate DbContext instances for operations that execute in parallel.

我的补充

  • 后台任务、消息消费者、批处理、Blazor 组件这类场景,比典型 MVC 请求更容易踩到这个坑,因为“一个逻辑流程里启动多个异步分支”很常见。
  • 如果代码里已经出现 “A second operation was started on this context instance...” 这类错误,优先排查有没有漏掉 await,以及是否把同一个 DbContext 传给了多个并行任务。
  • DbContext 一旦因为并发误用进入异常状态,通常不值得继续复用;最稳妥的处理方式是丢弃当前实例并重新创建。

ref

  • 来源:WuCai highlights + page note