web-dev-qa-db-fra.com

Contexte de données EF - Async / Await & Multithreading

J'utilise fréquemment async/wait pour garantir que les threads de l'API Web ASP.NET MVC ne sont pas bloqués par des opérations d'E/S et de réseau plus longues, en particulier des appels de base de données.

L'espace de noms System.Data.Entity fournit ici une variété d'extensions d'assistance, telles que FirstOrDefaultAsync , ContientAsync , CountAsync et ainsi de suite.

Cependant, comme les contextes de données ne sont pas sûrs pour les threads, cela signifie que le code suivant est problématique:

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

En fait, je vois parfois des exceptions telles que:

System.InvalidOperationException: la connexion n'a pas été fermée. L'état actuel de la connexion est ouvert.

Le modèle correct est-il alors d'utiliser un bloc using(new DbContext...) distinct pour chaque appel asynchrone à la base de données? Est-il potentiellement plus avantageux d'exécuter simplement synchrone à la place?

47
Alex

Nous sommes dans une impasse ici. AspNetSynchronizationContext, qui est responsable du modèle de thread d'un environnement d'exécution d'API Web ASP.NET, ne garantit pas que la poursuite asynchrone après await aura lieu sur le même thread. L'idée est de rendre les applications ASP.NET plus évolutives, donc moins de threads de ThreadPool sont bloqués avec des opérations synchrones en attente.

Cependant, la classe DataContext (qui fait partie de LINQ to SQL) n'est pas adaptée aux threads, elle ne doit donc pas être utilisée lorsqu'un commutateur de thread peut potentiellement se produire sur DataContext Appels API. Une construction using distincte par appel asynchrone n'aidera pas :

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

C'est parce que DataContext.Dispose Peut être exécuté sur un thread différent de celui sur lequel l'objet a été créé à l'origine, et ce n'est pas quelque chose que DataContext attendrait.

Si vous souhaitez vous en tenir à l'API DataContext, l'appeler de manière synchrone semble être la seule option possible. Je ne sais pas si cette instruction doit être étendue à l'ensemble de l'API EF, mais je suppose que tous les objets enfants créés avec l'API DataContext ne sont probablement pas non plus thread-safe. Ainsi, dans ASP.NET, leur portée using doit être limitée à celle entre deux appels await adjacents.

Il peut être tentant de décharger un tas d'appels synchrones DataContext vers un thread séparé avec await Task.Run(() => { /* do DataContext stuff here */ }). Cependant, ce serait n anti-modèle conn , en particulier dans le contexte d'ASP.NET où cela ne pourrait que nuire aux performances et à l'évolutivité, car cela ne réduirait pas le nombre de threads requis pour répondre à la demande .

Malheureusement, bien que l'architecture asynchrone d'ASP.NET soit excellente, elle reste incompatible avec certaines API et modèles établis (par exemple, voici n cas similaire ). C'est particulièrement triste, car nous ne traitons pas ici de l'accès simultané à l'API, c'est-à-dire qu'il n'y a pas plus d'un thread qui essaie d'accéder à un objet DataContext en même temps.

Espérons que Microsoft abordera ce problème dans les futures versions du Framework.

[MISE À JOUR] À grande échelle cependant, il pourrait être possible de décharger la logique EF vers un processus distinct (exécuté en tant que service WCF) qui fournirait une API asynchrone thread-safe à la logique client ASP.NET. Un tel processus peut être orchestré avec un contexte de synchronisation personnalisé en tant que machine d'événements, similaire à Node.js. Il peut même exécuter un pool d'appartements de type Node.js, chaque appartement conservant l'affinité de thread pour les objets EF. Cela permettrait de bénéficier encore de l'API asynchrone EF.

[MISE À JOUR] Voici ne tentative pour trouver une solution à ce problème.

31
noseratio

La classe DataContext fait partie de LINQ to SQL. Il ne comprend pas async/await AFAIK et ne doit pas être utilisé avec les méthodes d'extension Entity Framework async.

La DbContext classe fonctionnera bien avec async tant que vous utilisez EF6 ou supérieur; cependant, vous ne pouvez exécuter qu'une seule opération (synchronisation ou async) par instance de DbContext à la fois. Si votre code utilise réellement DbContext, examinez la pile d'appels de votre exception et vérifiez toute utilisation simultanée (par exemple, Task.WhenAll).

Si vous êtes sûr que tous les accès sont séquentiels, veuillez publier une repro minimale et/ou la signaler en tant que bogue à Microsoft Connect.

58
Stephen Cleary