【问题标题】:Multithreaded DbContext Operations多线程 DbContext 操作
【发布时间】:2021-10-06 17:49:29
【问题描述】:

以下代码(在单个 DbContext 上)导致“在前一个操作完成之前在此上下文上启动了第二个操作”。

[HttpGet]
[Route("api/[controller]/circuit")]
public async Task<IEnumerable<object>> GetAllCircuits()
{
    var circuits = await Task.WhenAll((await _context.Circuits.ToListAsync()).Select(async x => new
    {
        x.Id,
        x.FastestLap,
        x.LengthInMiles,
        Country = await _context.Countries.FirstOrDefaultAsync(c => c.Id == x.CountryId),
        Map = await _context.Maps.FirstOrDefaultAsync(m => m.Id == x.MapId),
        Locations = await _context.Locations.Where(l => l.CircuitId == x.Id).ToListAsync()
    }));

    return circuits;
}

我能够通过删除async/awaitTask.WhenAll 部分并用.Result 替换它们来解决这个问题,这在.NET 中似乎是一个很大的禁忌。固定示例如下:

[HttpGet]
[Route("api/[controller]/circuit")]
public async Task<IEnumerable<object>> GetAllCircuits()
{
    var circuits = (await _context.Circuits.ToListAsync()).Select(x => new
    {
        x.Id,
        x.FastestLap,
        x.LengthInMiles,
        Country = _context.Countries.FirstOrDefaultAsync(c => c.Id == x.CountryId).Result,
        Map = _context.Maps.FirstOrDefaultAsync(m => m.Id == x.MapId).Result,
        Locations = _context.Locations.Where(l => l.CircuitId == x.Id).ToListAsync().Result
    });

    return circuits;
}

我的三个问题是:

  1. 为什么会这样?
  2. “固定”代码是否干净?如果没有,您能否提出更好的方法?
  3. 我可以只使用.ToList() 而不是异步变体吗?

谢谢!

【问题讨论】:

  • 你为什么不写一个“普通”的 LINQ to Entities 查询——里面没有异步的东西,IQueryable&lt;TResult&gt;,只是等待最终的ToListAsync()?并使用导航属性而不是这些“手动:加入。
  • 1.您得到异常的原因是因为 EF 不能保证它返回的数据仍然有效,因为另一个线程可能已经更新/删除了数据库中的数据。 2. 在异步函数上调用.Result 比只调用函数的同步版本更糟糕(即ToList 而不是ToListAsync)。 3. 是的,但这样做会破坏异步编程的全部目的。如果您使用外键设置数据库表,您应该更喜欢导航属性而不是从上下文手动加载(正如@IvanStoev 所说)

标签: c# multithreading .net-core async-await entity-framework-core


【解决方案1】:

为什么会这样?

DbContext 不允许在同一个数据库连接上进行多个操作。在这种情况下,您有一个呼叫 (ToListAsync),然后是多个并发呼叫 (Select)。

“固定”代码是否干净?如果没有,请您提出更好的方法吗?

没有。你不应该使用.Result

您的选择是:

  1. (理想)更改 LINQ 查询,使其在 one 查询中包含所有必要信息,例如,使用连接或包含。这是理想的解决方案,因为只有一个查询,数据库服务器可以最有效地处理它。
  2. 一次只能执行一项操作,因为您只有一个连接。这就是.Result 工作的原因,但更好的解决方案是使用await,一次只做一个,而不是使用SelectTask.WhenAll。这种方法的缺点是一次只能执行一项操作。
  3. 保留多个操作,每个操作打开一个数据库连接。这种方法的缺点是它需要多个数据库连接。

我可以只使用 .ToList() 而不是异步变体吗?

ToListAsync 不是问题所在。问题是Select + WhenAll

【讨论】:

  • 非常感谢!有时我想它只是退后一步来看看更广泛的问题,而不是直接的错误。我现在就试试第一个选项:)