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

Posted

技术标签:

【中文标题】EF 数据上下文 - 异步/等待和多线程【英文标题】:EF Data Context - Async/Await & Multithreading 【发布时间】:2014-01-23 15:54:21 【问题描述】:

我经常使用 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: 连接未关闭。连接的当前状态是打开的。

然后为 each 对数据库的异步调用使用单独的 using(new DbContext...) 块是正确的模式吗?那么只执行同步是否可能更有益?

【问题讨论】:

【参考方案1】:

我们在这里陷入了僵局。负责 ASP.NET Web API 执行环境的线程模型的AspNetSynchronizationContext 不保证await 之后的异步继续会发生在同一个线程上。这样做的整个想法是使 ASP.NET 应用程序更具可扩展性,从而减少来自 ThreadPool 的线程被挂起的同步操作阻塞。

但是,DataContext class (part of 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 */ ) 的单独线程可能很诱人。但是,那是a known anti-pattern,尤其是在 ASP.NET 环境中,它可能只会损害性能和可伸缩性,因为它不会减少满足请求所需的线程数。

不幸的是,虽然 ASP.NET 的异步架构很棒,但它仍然与一些已建立的 API 和模式不兼容(例如,这里是 a similar case)。 这尤其令人难过,因为我们在这里不处理并发 API 访问,即不超过一个线程同时尝试访问 DataContext 对象。

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

[UPDATE] 不过,在大规模情况下,可以将 EF 逻辑卸载到一个单独的进程(作为 WCF 服务运行),这将为ASP.NET 客户端逻辑。这样的过程可以使用自定义同步上下文作为事件机器进行编排,类似于 Node.js。它甚至可以运行一个类似 Node.js 的单元池,每个单元维护 EF 对象的线程亲和性。这将允许仍然从异步 EF API 中受益。

[UPDATE]这里是some attempt,可以找到解决此问题的方法。

【讨论】:

这真是令人失望。 one 元素可以让 Web 应用程序从异步性中获得真正的好处是数据库调用......但不支持 EF。 “在与最初创建对象的线程不同的线程上,这不是 DataContext 所期望的。”您能否解释一下,DbContext 是否记得创建它的线程?究竟是什么问题? @alpha-mouse,AFAIR 无需深入了解the sources,它依赖于一些辅助类,这些类又使用线程本地存储 (TLS)。这可能在最近的 EF 版本中有所改变。 @Simon_Weaver, TransactionScopeAsyncFlowOption 将解决 TransactionScope 的类似问题,但 AFAIK 与 DataContext 无关。请注意,OP 已编辑问题并将DataContext 替换为DbContext,并且DbContext 已经async 友好(但不是cuncurrency-friendldy,请参阅斯蒂芬的回答)。 @Noseratio 谢谢。我现在只是作弊并删除了同步,但是我的意思是 DbContext。尝试优化一些古老的 Linq2Sql【参考方案2】:

DataContext class 是 LINQ to SQL 的一部分。它不理解async/await AFAIK,不应与实体框架async 扩展方法一起使用。

只要您使用的是 EF6 或更高版本,DbContext class 就可以与 async 一起正常工作;但是,每个DbContext 实例一次只能运行一个操作(同步或异步)。如果您的代码实际上正在使用DbContext,则检查您的异常的调用堆栈并检查任何并发使用情况(例如,Task.WhenAll)。

如果您确定所有访问都是按顺序进行的,那么请发布一个最小的重现和/或将其作为错误报告给 Microsoft Connect。

【讨论】:

我找不到任何具体的东西,但example they provide 确实在await 处跳转线程。他们确实声明他们是not making it fully threadsafe。 @Noseratio 哇,所以你不能等待两个挂起的 SQL 请求使用 Task.WhenAll 完成?我还没有尝试过使用异步 EF,但这对我来说似乎有点令人失望。 @Noseratio 因此,将异步 API 与 EF 结合使用的独特之处在于为长时间运行的请求提供了更大的可伸缩性(因为您不需要阻塞线程池中的线程等待请求完成) .这只是异步/等待的两个重要优势中的 一个,即可扩展性和性能。如果您不能通过同时查询多个请求来提高性能(SQL Server 可以完美地处理这个问题),那将非常令人失望。无论如何,感谢所有关于这个主题的问题/答案,这很有趣。 我相信你可以同时做多个请求,只要它们各自有自己的DbContext @AnshulNigam:如果“它”是指DbContext,那么您只需确保您的目标是.NET 4.5 并使用async/await,它就可以正常工作.【参考方案3】:

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

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

例子:

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

【讨论】:

不是问题的答案。

以上是关于EF 数据上下文 - 异步/等待和多线程的主要内容,如果未能解决你的问题,请参考以下文章

C# EF6 使用 Unity 对一个上下文进行多次异步调用 - Asp.Net Web Api

导致死锁的异步/等待示例

异步和多线程

异步和多线程的关系

异步和多线程的关系

python多进程和多线程的区别