EF数据上下文 - 异步/等待和多线程

58

我经常使用 async/await 来确保 ASP.NET MVC Web API 线程不会被较长时间的 I/O 和网络操作(尤其是数据库调用)阻塞。

System.Data.Entity 命名空间在这方面提供了各种辅助扩展,例如 FirstOrDefaultAsyncContainsAsyncCountAsync 等等。

然而,由于数据上下文不是线程安全的,这意味着以下代码存在问题:

var dbContext = new DbContext();
var something = await dbContext.someEntities.FirstOrDefaultAsync(e => e.Id == 1);
var morething = await dbContext.someEntities.FirstOrDefaultAsync(e => e.Id == 2);

实际上,我有时会看到以下异常情况:

System.InvalidOperationException: 连接没有关闭。连接的当前状态为打开。

那么正确的模式是为每个异步调用数据库使用单独的 using(new DbContext...) 块吗?还是直接执行同步操作更有益呢?

3个回答

69

DataContext是LINQ to SQL的一部分。据我所知,它不支持async/await,不应与Entity Framework的async扩展方法一起使用。

DbContext在使用EF6或更高版本时可以很好地支持async;但是,每个DbContext实例同时只能运行一个操作(同步或异步)。如果你的代码实际上使用了DbContext,那么请检查异常的调用堆栈并检查是否存在任何并发使用(例如Task.WhenAll)。

如果你确定所有访问都是顺序的,请发布一个最小的复现版本和/或将其报告为Microsoft Connect的错误。


1
我没有找到具体的内容,但是他们提供的示例确实在await处跳线程。他们确实声明了他们不会使其完全线程安全 - Stephen Cleary
1
@Noseratio 哇,你不能使用Task.WhenAll等待两个挂起的SQL请求完成吗?我还没有尝试过使用异步EF,但这对我来说有点令人失望。 - ken2k
3
@Noseratio,使用 EF 的异步 API 独特之处在于对长时间请求具有更高的可扩展性(因为您不需要阻止线程池中的线程等待请求完成)。这只是异步/等待的两个重要优势之一,即可扩展性和性能。如果无法通过同时查询多个请求来提高性能(SQL Server 可以完美处理此问题),那将非常令人失望。无论如何,感谢关于此主题的所有问题/答案,非常有趣。 - ken2k
9
我相信你可以同时处理多个请求,只要每个请求都有自己的 DbContext - Stephen Cleary
1
@AnshulNigam:如果你所说的“它”是指DbContext,那么你只需要确保你的目标是.NET 4.5并使用async/await,它就可以正常工作。 - Stephen Cleary
显示剩余10条评论

43
我们目前处于僵局。 AspNetSynchronizationContext 负责 ASP.NET Web API 执行环境的线程模型,但并不保证经过 await 后异步继续运行在同一线程上。这样做的整个思想是使 ASP.NET 应用程序更具可扩展性,因此较少使用 ThreadPool 中被挂起的同步操作。

然而,DataContext 类(LINQ to SQL 的一部分)并不是线程安全的,因此不应在可能在 DataContext API 调用之间发生线程切换的地方使用它。每个异步调用的单独using 构造也无法帮助解决问题:

var something;
using (var dataContext = new DataContext())
{
    something = await dataContext.someEntities.FirstOrDefaultAsync(e => e.Id == 1);
}

那是因为 DataContext.Dispose 可能在与创建对象的线程不同的线程上执行,这不是 DataContext 期望的情况。
如果您想坚持使用 DataContext API,则调用其同步函数似乎是唯一可行的选项。我不确定这个语句是否应该扩展到整个 EF API,但我想使用 DataContext API 创建的任何子对象可能也不是线程安全的。因此,在 ASP.NET 中,它们的 using 范围应该仅限于两个相邻的 await 调用之间。
将一堆同步的 DataContext 调用转移到一个单独的线程中可能很诱人,例如 await Task.Run(() => { /* do DataContext stuff here */ }) 。然而,在 ASP.NET 的背景下,这是 已知的反模式,可能只会损害性能和可扩展性,因为它不会减少满足请求所需的线程数量。

不幸的是,虽然ASP.NET的异步架构非常优秀,但它仍然与一些已经建立的API和模式不兼容(例如,这里有一个类似的案例)。

这真的很遗憾,因为我们在这里没有处理并发API访问,即没有超过一个线程同时尝试访问DataContext对象。

希望微软在未来的框架版本中会解决这个问题。

[更新] 尽管如此,在大规模上,可能可以将EF逻辑卸载到一个单独的进程中(作为WCF服务运行),该进程将为ASP.NET客户端逻辑提供线程安全的异步API。这样的进程可以使用自定义同步上下文作为事件机器进行编排,类似于Node.js。它甚至可以运行一组类似于Node.js的公寓,每个公寓维护EF对象的线程亲和性。这将允许仍然从异步EF API中受益。

[更新] 这里有一些尝试找到解决此问题的方法。


10
这真是令人失望。网络应用程序唯一可以从异步性中获得真正好处的元素是数据库调用......但 EF 不支持这一点。 - Alex
1
“在一个不同的线程上,而不是对象最初创建的线程上执行操作,这不是DataContext所期望的。” 请问,DbContext是否记得创建它的线程?问题具体是什么? - alpha-mouse
@alpha-mouse,如果不深入研究源代码,我记得它依赖于一些辅助类,这些类又使用线程本地存储(TLS)。最近的EF版本可能已经发生了改变。 - noseratio - open to work
2
@Simon_Weaver,TransactionScopeAsyncFlowOption可以解决TransactionScope的类似问题,但据我所知它与DataContext无关。请注意,OP已编辑问题并将DataContext替换为DbContext,而DbContext已经是async友好的(但不是并发友好的,请参见Stephen的答案)。 - noseratio - open to work
1
@Noseratio 谢谢。我现在只是作弊了,删除了同步,但是我的确是指 DbContext。尝试优化一些古老的 Linq2Sql。 - Simon_Weaver
显示剩余2条评论

-3

异步编程是一种并行编程的方式,其中一个工作单元在主应用程序线程之外运行,并通知调用线程其完成、失败或进度。使用异步编程可以获得的主要优势是提高应用程序性能和响应能力。

Entity Framework 6.0支持异步编程,这意味着您可以异步地查询数据和保存数据。通过使用async/await,您可以轻松编写Entity Framework的异步编程。

示例:

public async Task<Project> GetProjectAsync(string name) 
{
    DBContext _localdb = new DBContext();
    return await _localdb.Projects.FirstOrDefaultAsync(x => x.Name == name);
}

https://rajeevdotnet.blogspot.com/2018/06/entity-framework-asynchronous.html


2
不是问题的答案。 - trenthaynes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接