EF 数据上下文 - Async/Await &多线程 [英] EF Data Context - Async/Await & Multithreading

查看:72
本文介绍了EF 数据上下文 - Async/Await &多线程的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我经常使用 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...)块是正确的模式吗?那么只执行同步是否可能更有益?

解决方案

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

但是,DataContext 类(LINQ to SQL 的一部分)不是线程安全的,因此不应在可能跨 DataContext API 调用发生线程切换的情况下使用它.每个异步调用单独的 using 构造将没有帮助:

var something;使用 (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 调用之间.

使用 await Task.Run(() => {/* do DataContext stuff here */} 将一堆同步 DataContext 调用卸载到一个单独的线程可能很诱人).但是,这将是一种已知的反模式,尤其是在 ASP.NET 环境中,它可能只会损害性能和可伸缩性,因为它不会减少满足请求所需的线程数.

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

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

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

[更新]这里是一些尝试来寻找此问题的解决方案.

I frequently use async/await to ensure ASP.NET MVC Web API threads are not blocked by longer-running I/O and network operations, specifically database calls.

The System.Data.Entity namespace provides a variety of helper extensions here, such as FirstOrDefaultAsync, ContainsAsync, CountAsync and so forth.

However, since data contexts are not thread safe, this means that the following code is problematic:

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

In fact, I'm sometimes seeing exceptions such as:

System.InvalidOperationException: The connection was not closed. The connection's current state is open.

Is the correct pattern then to use a separate using(new DbContext...) block for each asynchronous call to the database? Is it potentially more beneficial to just execute synchronous then instead?

解决方案

We have a stalemate situation here. AspNetSynchronizationContext, which is responsible for the threading model of an ASP.NET Web API execution environment, does not guarantee that asynchronous continuation after await will take place on the same thread. The whole idea of this is to make ASP.NET apps more scalable, so less threads from ThreadPool are blocked with pending synchronous operations.

However, the DataContext class (part of LINQ to SQL ) is not thread-safe, so it shouldn't be used where a thread switch may potentially occurr across DataContext API calls. A separate using construct per asynchronous call will not help, either:

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

That's because DataContext.Dispose might be executed on a different thread from the one the object was originally created on, and this is not something DataContext would expect.

If you like to stick with the DataContext API, calling it synchronously appears to be the only feasible option. I'm not sure if that statement should be extended to the whole EF API, but I suppose any child objects created with DataContext API are probably not thread-safe, either. Thus, in ASP.NET their using scope should be limited to that of between two adjacent await calls.

It might be tempting to offload a bunch of synchronous DataContext calls to a separate thread with await Task.Run(() => { /* do DataContext stuff here */ }). However, that'd be a known anti-pattern, especially in the context of ASP.NET where it might only hurt performance and scalability, as it would not reduce the number of threads required to fulfill the request.

Unfortunately, while the asynchronous architecture of ASP.NET is great, it remains being incompatible with some established APIs and patterns (e.g., here is a similar case). That's especially sad, because we're not dealing with concurrent API access here, i.e. no more than one thread is trying to access a DataContext object at the same time.

Hopefully, Microsoft will address that in the future versions of the Framework.

[UPDATE] On a large scale though, it might be possible to offload the EF logic to a separate process (run as a WCF service) which would provide a thread-safe async API to the ASP.NET client logic. Such process can be orchestrated with a custom synchronization context as an event machine, similar to Node.js. It may even run a pool of Node.js-like apartments, each apartment maintaining the thread affinity for EF objects. That would allow to still benefit from the async EF API.

[UPDATE] Here is some attempt to find a solution to this problem.

这篇关于EF 数据上下文 - Async/Await &amp;多线程的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆