Подтвердить что ты не робот

Контекст данных EF - Async/Await & Multithreading

Я часто использую async/await, чтобы гарантировать, что потоки ASP.NET MVC Web API не блокируются более длительными операциями ввода-вывода и сети, в частности вызовы базы данных.

Пространство имен System.Data.Entity предоставляет здесь множество вспомогательных расширений, таких как FirstOrDefaultAsync, ContainsAsync, CountAsync и т.д.

Однако, поскольку контексты данных не являются потокобезопасными, это означает, что следующий код проблематичен:

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...) для каждого асинхронного вызова в базе данных? Возможно ли более выгодно вместо этого выполнять синхронное?

4b9b3361

Ответ 1

Здесь у нас есть тупиковая ситуация. AspNetSynchronizationContext, который отвечает за модель потоковой обработки среды выполнения ASP.NET Web API, не гарантирует, что асинхронное продолжение после await произойдет в одном потоке. Вся идея заключается в том, чтобы сделать приложения ASP.NET более масштабируемыми, поэтому меньше потоков из ThreadPool блокируются ожидающими синхронными операциями.

Однако DataContext не является потокобезопасным, поэтому его не следует использовать, если потенциальный коммутатор потоков может возникать в вызовах API DataContext. Отдельная конструкция using для асинхронного вызова будет иметь не, либо:

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

Это потому, что DataContext.Dispose может выполняться в другом потоке с тем, из которого был первоначально создан объект, и это не то, что ожидало бы DataContext.

Если вам нравится придерживаться API DataContext, вызов его синхронно представляется единственным возможным вариантом. Я не уверен, что это утверждение должно быть распространено на весь API EF, но я полагаю, что любые дочерние объекты, созданные с помощью DataContext API, также не являются потокобезопасными. Таким образом, в ASP.NET их область using должна быть ограничена областью между двумя соседними вызовами await.

Может возникнуть соблазн выгрузить кучу синхронных вызовов DataContext в отдельный поток с помощью await Task.Run(() => { /* do DataContext stuff here */ }). Тем не менее, это будет известный анти-шаблон, особенно в контексте ASP.NET, где это может только повредить производительность и масштабируемость, поскольку это не уменьшит количество потоков, необходимых для выполнения запроса.

К сожалению, хотя асинхронная архитектура ASP.NET велика, она по-прежнему несовместима с некоторыми установленными API и шаблонами (например, здесь аналогичный случай). Это особенно печально, потому что мы не имеем дело с одновременным доступом к API здесь, то есть не более одного потока пытается получить доступ к объекту DataContext в то же время.

Надеюсь, Microsoft рассмотрит это в будущих версиях Framework.

[UPDATE] Однако в больших масштабах может быть возможно разгрузить логику EF отдельному процессу (выполняемому как служба WCF), который предоставил бы безопасный асинхронный API для потоков Логика клиента ASP.NET. Такой процесс может быть организован с помощью настраиваемого контекста синхронизации как машины событий, аналогичной Node.js. Он может даже запускать пул квартир Node.js-like, каждая квартира поддерживает сходство потоков для объектов EF. Это позволит по-прежнему извлекать выгоду из API асинхронного EF.

[ОБНОВЛЕНИЕ] Ниже приведена некоторая попытка найти решение этой проблемы.

Ответ 2

DataContext класс является частью LINQ to SQL. Он не понимает async/await AFAIK и не должен использоваться с методами расширения Entity Framework async.

Класс DbContext будет отлично работать с async, если вы используете EF6 или выше; Тем не менее, вы можете выполнять только одну операцию (синхронизация или асинхронный) за экземпляр DbContext, запущенный за раз. Если ваш код на самом деле использует DbContext, затем просмотрите стек вызовов из вашего исключения и проверьте наличие параллельного использования (например, Task.WhenAll).

Если вы уверены, что весь доступ последователен, то, пожалуйста, отправьте минимальный реестр и/или сообщите об этом как ошибку для Microsoft Connect.