Entity Framework SaveChanges () contre SaveChangesAsync () et Find () contre FindAsync ()


87

J'ai recherché les différences entre 2 paires ci-dessus mais je n'ai trouvé aucun article expliquant clairement à ce sujet ainsi que quand utiliser l'un ou l'autre.

Alors, quelle est la différence entre SaveChanges()et SaveChangesAsync()?
Et entre Find()et FindAsync()?

Côté serveur, lorsque nous utilisons des Asyncméthodes, nous devons également ajouter await. Ainsi, je ne pense pas que ce soit asynchrone côté serveur.

Cela aide-t-il uniquement à empêcher le blocage de l'interface utilisateur sur le navigateur côté client? Ou y a-t-il des avantages et des inconvénients entre eux?


2
async est beaucoup, beaucoup plus que d' arrêter le thread d'interface client de bloquer dans les applications clientes. Je suis sûr qu'une réponse d'expert sera bientôt disponible.
jdphenix

Réponses:


175

Chaque fois que vous devez effectuer une action sur un serveur distant, votre programme génère la requête, l'envoie, puis attend une réponse. Je vais utiliser SaveChanges()et SaveChangesAsync()comme exemple, mais il en va de même pour Find()et FindAsync().

Supposons que vous ayez une liste myListde plus de 100 éléments que vous devez ajouter à votre base de données. Pour insérer cela, votre fonction ressemblerait à ceci:

using(var context = new MyEDM())
{
    context.MyTable.AddRange(myList);
    context.SaveChanges();
}

Commencez par créer une instance de MyEDM, ajoutez la liste myListà la table MyTable, puis appelez SaveChanges()pour conserver les modifications apportées à la base de données. Cela fonctionne comme vous le souhaitez, les enregistrements sont validés, mais votre programme ne peut rien faire d'autre tant que la validation n'est pas terminée. Cela peut prendre du temps en fonction de ce que vous vous engagez. Si vous validez des modifications dans les enregistrements, l'entité doit les valider un par un (une fois, une sauvegarde a pris 2 minutes pour les mises à jour)!

Pour résoudre ce problème, vous pouvez effectuer l'une des deux opérations suivantes. Le premier est que vous pouvez démarrer un nouveau fil pour gérer l'insert. Bien que cela libère le thread appelant pour continuer à s'exécuter, vous avez créé un nouveau thread qui va simplement rester là et attendre. Il n'y a pas besoin de cette surcharge, et c'est ce que le async awaitmodèle résout.

Pour les opérations d'E / S, awaitdevient rapidement votre meilleur ami. En prenant la section de code ci-dessus, nous pouvons la modifier pour qu'elle soit:

using(var context = new MyEDM())
{
    Console.WriteLine("Save Starting");
    context.MyTable.AddRange(myList);
    await context.SaveChangesAsync();
    Console.WriteLine("Save Complete");
}

C'est un tout petit changement, mais il y a des effets profonds sur l'efficacité et les performances de votre code. Alors que se passe-t-il? Le début du code est le même, vous créez une instance de MyEDMet ajoutez votre myListà MyTable. Mais lorsque vous appelez await context.SaveChangesAsync(), l'exécution du code revient à la fonction appelante! Ainsi, pendant que vous attendez que tous ces enregistrements soient validés, votre code peut continuer à s'exécuter. Dites que la fonction qui contenait le code ci-dessus avait la signature de public async Task SaveRecords(List<MyTable> saveList), la fonction appelante pourrait ressembler à ceci:

public async Task MyCallingFunction()
{
    Console.WriteLine("Function Starting");
    Task saveTask = SaveRecords(GenerateNewRecords());

    for(int i = 0; i < 1000; i++){
        Console.WriteLine("Continuing to execute!");
    }

    await saveTask;
    Console.Log("Function Complete");
}

Pourquoi vous auriez une fonction comme celle-ci, je ne sais pas, mais ce qu'elle produit montre comment async awaitfonctionne. Voyons d'abord ce qui se passe.

L'exécution entre MyCallingFunction, Function Startingpuis Save Startingest écrite sur la console, puis la fonction SaveChangesAsync()est appelée. À ce stade, l'exécution retourne MyCallingFunctionet entre dans la boucle for en écrivant «Continuer à exécuter» jusqu'à 1000 fois. Une fois SaveChangesAsync()terminé, l'exécution revient à la SaveRecordsfonction, en écrivant Save Completedans la console. Une fois que tout est SaveRecordsterminé, l'exécution se poursuivra MyCallingFunctioncomme elle l'était à la SaveChangesAsync()fin. Confus? Voici un exemple de sortie:

Démarrage de la fonction
Enregistrer le départ
Continuer à exécuter!
Continuer à exécuter!
Continuer à exécuter!
Continuer à exécuter!
Continuer à exécuter!
....
Continuer à exécuter!
Sauvegardez terminé!
Continuer à exécuter!
Continuer à exécuter!
Continuer à exécuter!
....
Continuer à exécuter!
Fonction terminée!

Ou peut-être:

Démarrage de la fonction
Enregistrer le départ
Continuer à exécuter!
Continuer à exécuter!
Sauvegardez terminé!
Continuer à exécuter!
Continuer à exécuter!
Continuer à exécuter!
....
Continuer à exécuter!
Fonction terminée!

C'est la beauté de async await, votre code peut continuer à s'exécuter pendant que vous attendez que quelque chose se termine. En réalité, vous auriez une fonction plus comme celle-ci que votre fonction d'appel:

public async Task MyCallingFunction()
{
    List<Task> myTasks = new List<Task>();
    myTasks.Add(SaveRecords(GenerateNewRecords()));
    myTasks.Add(SaveRecords2(GenerateNewRecords2()));
    myTasks.Add(SaveRecords3(GenerateNewRecords3()));
    myTasks.Add(SaveRecords4(GenerateNewRecords4()));

    await Task.WhenAll(myTasks.ToArray());
}

Ici, vous disposez de quatre fonctions de sauvegarde d'enregistrement différentes en même temps . MyCallingFunctionse terminera beaucoup plus rapidement async awaitque si les SaveRecordsfonctions individuelles étaient appelées en série.

La seule chose que je n'ai pas encore abordée est le awaitmot clé. Cela empêche la fonction en cours de s'exécuter jusqu'à ce que tout ce que Taskvous attendez soit terminé. Donc, dans le cas de l'original MyCallingFunction, la ligne Function Completene sera pas écrite sur la console tant que la SaveRecordsfonction ne sera pas terminée.

Pour faire court, si vous avez une option à utiliser async await, vous devriez car cela augmentera considérablement les performances de votre application.


7
99% du temps, je dois encore attendre que les valeurs soient reçues de la base de données avant de pouvoir continuer. Dois-je toujours utiliser async? Async permet-il à 100 personnes de se connecter à mon site Web de manière asynchrone? Si je n'utilise pas async, cela signifie-t-il que les 100 utilisateurs doivent attendre la ligne 1 à la fois?
MIKE

6
À noter: la création d'un nouveau thread à partir du pool de threads fait d'ASP un triste panda puisque vous volez essentiellement un thread d'ASP (ce qui signifie que le thread ne peut pas gérer d'autres demandes ou faire quoi que ce soit car il est bloqué dans un appel de blocage). Si vous utilisez awaitcependant, même si VOUS n'avez rien d'autre à faire après l'appel à SaveChanges, ASP dira "aha, ce thread est retourné en attente d'une opération asynchrone, cela signifie que je peux laisser ce thread gérer une autre requête en attendant ! " Cela améliore considérablement l'échelle horizontale de votre application.
sara

3
En fait, j'ai évalué l'async comme étant plus lent. Et avez-vous déjà vu combien de threads sont disponibles dans un serveur ASP.Net typique? C'est comme des dizaines de milliers. Ainsi, les chances de manquer de threads pour gérer d'autres demandes sont très peu probables et même si vous avez suffisamment de trafic pour saturer tous ces threads, votre serveur est-il vraiment assez puissant pour ne pas se déformer dans ce cas de toute façon? Affirmer que l'utilisation de l'asynchrone partout augmente les performances est totalement faux. Cela peut dans certains scénarios, mais dans la plupart des situations courantes, ce sera en fait plus lent. Benchmark et voir.
user3766657

@MIKE alors qu'un seul utilisateur doit attendre que la base de données renvoie des données pour continuer, les autres utilisateurs qui utilisent votre application ne le font pas. Bien que IIS crée un thread pour chaque demande (en fait, c'est plus complexe que cela), votre thread en attente peut être utilisé pour gérer d'autres demandes, ce qui est important pour l'évolutivité afaik. Imagerie de chaque requête, au lieu d'utiliser 1 thread à plein temps, il utilise de nombreux threads plus courts qui peuvent être réutilisés ailleurs (c'est-à-dire une autre requête).
Bart Calixto

1
Je voudrais juste ajouter que vous devez toujours await pour SaveChangesAsynccar EF ne prend pas en charge multiple enregistre en même temps. docs.microsoft.com/en-us/ef/core/saving/async En outre, il y a en fait un grand avantage à utiliser ces méthodes asynchrones. Par exemple, vous pouvez continuer à recevoir d'autres demandes dans votre webApi lorsque vous enregistrez des données ou faites beaucoup de sutuff, ou améliorez l'expérience utilisateur en ne gelant pas l'interface lorsque vous êtes dans une application de bureau.
tgarcia

1

Ma dernière explication sera basée sur l'extrait de code suivant.

using System;
using System.Threading;
using System.Threading.Tasks;
using static System.Console;

public static class Program
{
    const int N = 20;
    static readonly object obj = new object();
    static int counter;

    public static void Job(ConsoleColor color, int multiplier = 1)
    {
        for (long i = 0; i < N * multiplier; i++)
        {
            lock (obj)
            {
                counter++;
                ForegroundColor = color;
                Write($"{Thread.CurrentThread.ManagedThreadId}");
                if (counter % N == 0) WriteLine();
                ResetColor();
            }
            Thread.Sleep(N);
        }
    }

    static async Task JobAsync()
    {
       // intentionally removed
    }

    public static async Task Main()
    {
       // intentionally removed
    }
}

Cas 1

static async Task JobAsync()
{
    Task t = Task.Run(() => Job(ConsoleColor.Red, 1));
    Job(ConsoleColor.Green, 2);
    await t;
    Job(ConsoleColor.Blue, 1);
}

public static async Task Main()
{
    Task t = JobAsync();
    Job(ConsoleColor.White, 1);
    await t;
}

entrez la description de l'image ici

Remarques: Comme la partie synchrone (verte) de JobAsynctourne plus longtemps que la tâche t(rouge), la tâche test déjà terminée au point de await t. En conséquence, la suite (bleue) fonctionne sur le même fil que le vert. La partie synchrone de Main(blanche) tournera une fois la partie verte terminée. C'est pourquoi la partie synchrone en méthode asynchrone est problématique.

Cas 2

static async Task JobAsync()
{
    Task t = Task.Run(() => Job(ConsoleColor.Red, 2));
    Job(ConsoleColor.Green, 1);
    await t;
    Job(ConsoleColor.Blue, 1);
}

public static async Task Main()
{
    Task t = JobAsync();
    Job(ConsoleColor.White, 1);
    await t;
}

entrez la description de l'image ici

Remarques: Ce cas est opposé au premier cas. La partie synchrone (verte) des JobAsyncspins plus courte que la tâche t(rouge) alors la tâche tn'est pas terminée au moment de await t. En conséquence, la suite (bleue) s'exécute sur le fil différent du fil vert. La partie synchrone de Main(blanche) tourne toujours une fois que la partie verte a fini de tourner.

Cas 3

static async Task JobAsync()
{
    Task t = Task.Run(() => Job(ConsoleColor.Red, 1));
    await t;
    Job(ConsoleColor.Green, 1);
    Job(ConsoleColor.Blue, 1);
}

public static async Task Main()
{
    Task t = JobAsync();
    Job(ConsoleColor.White, 1);
    await t;
}

entrez la description de l'image ici

Remarques: Ce cas résoudra le problème dans les cas précédents concernant la partie synchrone en méthode asynchrone. La tâche test immédiatement attendue. En conséquence, la suite (bleue) s'exécute sur le fil différent du fil vert. La partie synchrone de Main(blanc) tournera immédiatement parallèlement à JobAsync.

Si vous souhaitez ajouter d'autres cas, n'hésitez pas à les modifier.


1

Cette déclaration est incorrecte:

Côté serveur, lorsque nous utilisons des méthodes Async, nous devons également ajouter await.

Vous n'avez pas besoin d'ajouter "await", awaitc'est simplement un mot-clé pratique en C # qui vous permet d'écrire plus de lignes de code après l'appel, et ces autres lignes ne seront exécutées qu'une fois l'opération Save terminée. Mais comme vous l'avez souligné, vous pouvez accomplir cela simplement en appelant SaveChangesau lieu de SaveChangesAsync.

Mais fondamentalement, un appel asynchrone est bien plus que cela. L'idée ici est que s'il y a un autre travail que vous pouvez faire (sur le serveur) pendant que l'opération de sauvegarde est en cours, vous devez utiliser SaveChangesAsync. N'utilisez pas «attendre». Appelez simplement SaveChangesAsync, puis continuez à faire d'autres choses en parallèle. Cela inclut potentiellement, dans une application Web, le renvoi d'une réponse au client avant même la fin de l'enregistrement. Mais bien sûr, vous voudrez toujours vérifier le résultat final de la sauvegarde afin qu'en cas d'échec, vous puissiez le communiquer à votre utilisateur ou le consigner d'une manière ou d'une autre.


4
Vous voulez en fait attendre ces appels, sinon vous pourriez exécuter des requêtes et / ou enregistrer des données simultanément à l'aide de la même instance de DbContext et DbContext n'est pas thread-safe. En plus de cela, wait facilite la gestion des exceptions. Sans attendre, vous devrez stocker la tâche et vérifier si elle est défectueuse, mais sans savoir quand la tâche est terminée, vous ne sauriez pas quand vérifier à moins d'utiliser '.ContinueWith' qui nécessite beaucoup plus de réflexion qu'attendre.
Pawel

22
Cette réponse est trompeuse, appeler une méthode asynchrone sans attendre en fait un "feu et oublie". La méthode se déclenche et se terminera probablement un jour, mais vous ne saurez jamais quand, et si elle lève une exception, vous n'en entendrez jamais parler, vous ne pouvez pas synchroniser avec son achèvement. Ce type de comportement potentiellement dangereux doit être choisi, et non invoqué avec une règle simple (et incorrecte) comme «attend sur le client, pas d’attente sur le serveur».
John Melville

1
C'est une connaissance très utile que j'avais lue dans la documentation, mais que je n'avais pas vraiment envisagée. Ainsi, vous avez la possibilité de: 1. SaveChangesAsync () sur "Fire and forget", comme le dit John Melville ... ce qui m'est utile dans certains cas. 2. attendez SaveChangesAsync () to "Fire, revenez à l'appelant, puis exécutez du code 'post-save' une fois la sauvegarde terminée. Pièce très utile. Merci.
Parrhesia Joe
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.