You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

EF Core DbContext与Parallel.ForEachAsync并发操作异常排查

EF Core并发调用DbContext引发多线程异常问题

问题描述

已将MyDbContext和CarRepository注册为Transient生命周期,在CarController的Get接口中使用Parallel.ForEachAsync并发调用GetById方法时,抛出System.InvalidOperationException异常,提示同一DbContext实例被多线程并发操作。所有异步方法均已正确使用await,同步遍历无异常。

相关代码

CarRepository代码

public class CarRepository<Car> : ICarRepository
{
    // ... 其他成员
    public async Task<Car?> GetById(TId id, CancellationToken cancellationToken)
    {
        return await _dbContext.Cars().FindAsync(id, cancellationToken);            
    }
}

服务注册代码(Program.cs)

var builder = WebApplication.CreateBuilder(args);
// ... 其他配置
builder.Services.AddDbContext<MyDbContext>(options =>
{
   options.UseSqlServer("conn-string");
}, ServiceLifetime.Transient);

builder.Services.AddTransient<ICarRepository, CarRepository>();

DbContext定义

public class MyDbContext: DbContext
{
   public MyDbContext(DbContextOptions<MyDbContext> options)
        : base(options)
   {
        Database.EnsureCreated();            
   }
   // ... DbSet定义
}

Controller代码

public class CarController : ControllerBase
{
    private ICarRepository _repository;
    public CarController(ICarRepository repository)
    {
       _repository = repository;
    }

    [HttpGet]
    public async Task<IActionResult> Get()
    {
        ParallelOptions parallelOptions = new()
        {
            MaxDegreeOfParallelism = 3
        };

       var cars = new List<Car>();
       await Parallel.ForEachAsync(request.CarIds, parallelOptions, async (id, _) =>
       {
           Car? car = await _repository.GetById(id, cancellationToken);
           if (car != null)
           { 
             cars.Add(car); 
           }
       });

       return Ok();
    }
}

异常信息

System.InvalidOperationException: 在此上下文实例上启动第二个操作之前,前一个操作尚未完成。通常是由于不同线程同时使用同一个DbContext实例导致的。

问题原因

核心问题在于Controller的默认生命周期是Scoped:

  • 尽管MyDbContext和CarRepository注册为Transient,但在同一个HTTP请求的生命周期内,Controller实例是唯一的,其构造函数注入的_repository也是同一个实例,对应的DbContext自然也是同一个。
  • Parallel.ForEachAsync会同时启动多个并发任务,这些任务共享同一个DbContext实例进行数据库操作,而EF Core的DbContext本身是非线程安全的,因此触发了多线程操作异常。
  • 另外,代码中使用List<Car>存储结果,List同样是非线程安全的,并发调用Add会导致数据损坏(即使没有DbContext异常,也会有这个问题)。

解决方案

方案1:为每个并发任务创建独立的服务作用域(推荐)

通过IServiceScopeFactory在每个并行任务中创建新的作用域,获取独立的ICarRepository和DbContext实例,同时改用线程安全的ConcurrentBag<Car>存储结果:

public class CarController : ControllerBase
{
    private readonly IServiceScopeFactory _scopeFactory;

    public CarController(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }

    [HttpGet]
    public async Task<IActionResult> Get([FromQuery] List<int> carIds, CancellationToken cancellationToken)
    {
        ParallelOptions parallelOptions = new()
        {
            MaxDegreeOfParallelism = 3
        };

        var cars = new ConcurrentBag<Car>();
        await Parallel.ForEachAsync(carIds, parallelOptions, async (id, ct) =>
        {
            using var scope = _scopeFactory.CreateScope();
            var repository = scope.ServiceProvider.GetRequiredService<ICarRepository>();
            var car = await repository.GetById(id, ct);
            if (car != null)
            {
                cars.Add(car);
            }
        });

        return Ok(cars);
    }
}

方案2:改用批量查询替代并发调用(更优)

EF Core支持批量查询,一次请求即可获取所有目标数据,避免并发带来的线程安全问题,同时减少数据库连接开销:

首先在CarRepository中添加批量查询方法:

public async Task<List<Car>> GetByIds(IEnumerable<TId> ids, CancellationToken cancellationToken)
{
    return await _dbContext.Cars.Where(c => ids.Contains(c.Id)).ToListAsync(cancellationToken);
}

然后修改Controller代码:

[HttpGet]
public async Task<IActionResult> Get([FromQuery] List<int> carIds, CancellationToken cancellationToken)
{
    var cars = await _repository.GetByIds(carIds, cancellationToken);
    return Ok(cars);
}

方案3:将Controller改为Transient生命周期(不推荐)

通过修改Controller的生命周期为Transient,让每个请求的Controller实例不同,但这种方式会打破Controller默认的Scoped生命周期设计,可能引发其他依赖注入问题,因此不建议使用:

// Program.cs中添加以下代码
builder.Services.AddControllers().AddControllersAsServices();
builder.Services.AddTransient<CarController>();

总结

EF Core的DbContext本质是非线程安全的,即使注册为Transient,只要被同一个Scoped实例(如Controller)持有,就会在并发场景下被共享。解决的核心是确保每个并发操作拥有独立的DbContext实例,或者直接使用批量查询避免并发调用。

内容的提问来源于stack exchange,提问作者user1765862

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 15:40:37