Entity Framework SaveChanges () vs SaveChangesAsync () e Find () vs FindAsync ()


89

Ho cercato le differenze tra le 2 coppie di cui sopra, ma non ho trovato alcun articolo che spieghi chiaramente al riguardo e quando utilizzare l'una o l'altra.

Allora qual è la differenza tra SaveChanges()e SaveChangesAsync()?
E tra Find()e FindAsync()?

Sul lato server, quando usiamo Asyncmetodi, dobbiamo anche aggiungere await. Quindi, non penso che sia asincrono sul lato server.

Aiuta solo a prevenire il blocco dell'interfaccia utente sul browser lato client? O ci sono pro e contro tra di loro?


2
async è molto, molto di più che impedire al thread dell'interfaccia utente del client di bloccarsi nelle applicazioni client. Sono sicuro che presto arriverà una risposta esperta.
jdphenix

Risposte:


176

Ogni volta che è necessario eseguire un'azione su un server remoto, il programma genera la richiesta, la invia, quindi attende una risposta. Userò SaveChanges()e SaveChangesAsync()come esempio, ma lo stesso vale per Find()e FindAsync().

Supponi di avere un elenco myListdi oltre 100 elementi che devi aggiungere al tuo database. Per inserirlo, la tua funzione sarebbe simile a questa:

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

Per prima cosa crei un'istanza di MyEDM, aggiungi l'elenco myListalla tabella MyTable, quindi chiama SaveChanges()per rendere persistenti le modifiche al database. Funziona come vuoi, i record vengono salvati, ma il tuo programma non può fare nient'altro fino al termine del commit. Questo può richiedere molto tempo a seconda di ciò che stai impegnando. Se stai effettuando il commit delle modifiche ai record, l'entità deve eseguire il commit di quelle una alla volta (una volta ho avuto un salvataggio che richiedeva 2 minuti per gli aggiornamenti)!

Per risolvere questo problema, potresti fare una delle due cose. Il primo è che puoi avviare un nuovo thread per gestire l'inserto. Mentre questo libererà il thread chiamante per continuare l'esecuzione, hai creato un nuovo thread che rimarrà lì e aspetta. Non c'è bisogno di quel sovraccarico, e questo è ciò che lo async awaitschema risolve.

Per le operazioni di I / O, awaitdiventa rapidamente il tuo migliore amico. Prendendo la sezione del codice dall'alto, possiamo modificarla in modo che sia:

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

È un cambiamento molto piccolo, ma ci sono effetti profondi sull'efficienza e sulle prestazioni del codice. Allora cosa succede? L'inizio del codice è lo stesso, crei un'istanza di MyEDMe aggiungi il tuo myLista MyTable. Ma quando si chiama await context.SaveChangesAsync(), l'esecuzione del codice ritorna alla funzione chiamante! Quindi, mentre aspetti che tutti quei record vengano salvati, il tuo codice può continuare a essere eseguito. Supponiamo che la funzione che conteneva il codice precedente avesse la firma di public async Task SaveRecords(List<MyTable> saveList), la funzione chiamante potrebbe essere simile a questa:

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");
}

Perché avresti una funzione come questa, non lo so, ma ciò che produce mostra come async awaitfunziona. Per prima cosa esaminiamo cosa succede.

L'esecuzione entra MyCallingFunction, Function Startingquindi Save Startingviene scritta sulla console, quindi la funzione SaveChangesAsync()viene chiamata. A questo punto, l'esecuzione ritorna MyCallingFunctioned entra nel ciclo for scrivendo 'Continuing to Execute' fino a 1000 volte. Al SaveChangesAsync()termine, l'esecuzione ritorna alla SaveRecordsfunzione, scrivendo Save Completesulla console. Una volta che tutto è stato SaveRecordscompletato, l'esecuzione continuerà nel modo MyCallingFunctioncorretto dove era al SaveChangesAsync()termine. Confuso? Ecco un esempio di output:

Avvio della funzione
Salva avvio
Continuando a eseguire!
Continuando a eseguire!
Continuando a eseguire!
Continuando a eseguire!
Continuando a eseguire!
....
Continuando a eseguire!
Salva completato!
Continuando a eseguire!
Continuando a eseguire!
Continuando a eseguire!
....
Continuando a eseguire!
Funzione completa!

O forse:

Avvio della funzione
Salva avvio
Continuando a eseguire!
Continuando a eseguire!
Salva completato!
Continuando a eseguire!
Continuando a eseguire!
Continuando a eseguire!
....
Continuando a eseguire!
Funzione completa!

Questa è la bellezza di async await, il tuo codice può continuare a funzionare mentre aspetti che qualcosa finisca. In realtà, avresti una funzione più simile a questa come funzione chiamante:

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());
}

Qui sono disponibili quattro diverse funzioni di salvataggio dei record contemporaneamente . MyCallingFunctionsi completerà molto più velocemente usando async awaitche se le singole SaveRecordsfunzioni fossero chiamate in serie.

L'unica cosa che non ho ancora toccato è la awaitparola chiave. Ciò che fa è interrompere l'esecuzione della funzione corrente fino al completamento di tutto ciò Taskche stai aspettando. Quindi, nel caso dell'originale MyCallingFunction, la riga Function Completenon verrà scritta sulla console fino al SaveRecordstermine della funzione.

Per async awaitfarla breve, se hai un'opzione da usare , dovresti in quanto aumenterà notevolmente le prestazioni della tua applicazione.


7
Per il 99% del tempo devo ancora attendere la ricezione dei valori dal database prima di poter continuare. Dovrei continuare a usare l'asincronia? Async consente a 100 persone di connettersi al mio sito Web in modo asincrono? Se non utilizzo l'asincronia, significa che tutti i 100 utenti devono attendere in linea 1 alla volta?
MIKE

6
Vale la pena notare: la generazione di un nuovo thread dal pool di thread rende ASP un triste panda poiché sostanzialmente si ruba un thread da ASP (il che significa che il thread non può gestire altre richieste o fare nulla poiché è bloccato in una chiamata di blocco). Se usi awaittuttavia, anche se non hai bisogno di fare nient'altro dopo la chiamata a SaveChanges, ASP dirà "aha, questo thread è tornato in attesa di un'operazione asincrona, questo significa che posso lasciare che questo thread gestisca qualche altra richiesta nel frattempo ! " Questo rende la tua app scalabile orizzontalmente molto meglio.
sara

3
In realtà ho valutato l'asincronia per essere più lento. E hai mai visto quanti thread sono disponibili in un tipico server ASP.Net? Sono come decine di migliaia. Quindi le probabilità di esaurire i thread per gestire ulteriori richieste sono molto improbabili e anche se avessi abbastanza traffico per saturare tutti quei thread, il tuo server è davvero abbastanza potente da non piegarsi in quel caso comunque? Affermare che l'utilizzo di async ovunque aumenta le prestazioni è totalmente sbagliato. Può in alcuni scenari, ma nelle situazioni più comuni sarà in realtà più lento. Confronta e guarda.
user3766657

@MIKE mentre un singolo utente deve attendere che il database restituisca i dati per continuare, gli altri utenti che utilizzano la tua applicazione no. Mentre IIS crea un thread per ogni richiesta (in realtà è più complesso di così), il thread in attesa può essere utilizzato per gestire altre richieste, questo è importante per la scalabilità afaik. Immaginando ogni richiesta, invece di utilizzare 1 thread a tempo pieno, utilizza molti thread più brevi che possono essere riutilizzati da qualche altra parte (ovvero altre richieste).
Bart Calixto

1
Vorrei solo aggiungere che si dovrebbe sempre await per SaveChangesAsyncpoiché EF non supporta più di risparmiare allo stesso tempo. docs.microsoft.com/en-us/ef/core/saving/async Inoltre, c'è effettivamente un grande vantaggio nell'utilizzo di questi metodi asincroni. Ad esempio, puoi continuare a ricevere altre richieste nel tuo webApi quando salvi i dati o esegui molte operazioni, oppure puoi migliorare l'esperienza utente senza bloccare l'interfaccia quando sei in un'applicazione desktop.
tgarcia

1

La mia restante spiegazione sarà basata sul seguente frammento di codice.

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
    }
}

Caso 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;
}

inserisci qui la descrizione dell'immagine

Note: poiché la parte sincrona (verde) dei JobAsyncgiri più lunga dell'attività t(rossa), l'attività tè già completata al punto di await t. Di conseguenza, la continuazione (blu) viene eseguita sullo stesso thread di quella verde. La parte sincrona di Main(bianco) girerà dopo che quella verde avrà finito di girare. Ecco perché la parte sincrona nel metodo asincrono è problematica.

Caso 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;
}

inserisci qui la descrizione dell'immagine

Osservazioni: questo caso è opposto al primo. La parte sincrona (verde) di JobAsyncgiri più breve dell'attività t(rossa) quindi l'attività tnon è stata completata al punto di await t. Di conseguenza, la continuazione (blu) viene eseguita sul thread diverso come quello verde. La parte sincrona di Main(bianco) gira ancora dopo che quella verde ha finito di girare.

Caso 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;
}

inserisci qui la descrizione dell'immagine

Note: questo caso risolverà il problema nei casi precedenti sulla parte sincrona nel metodo asincrono. Il compito tè immediatamente atteso. Di conseguenza, la continuazione (blu) viene eseguita sul thread diverso come quello verde. La parte sincrona di Main(bianco) ruoterà immediatamente parallelamente a JobAsync.

Se desideri aggiungere altri casi, sentiti libero di modificare.


0

Questa affermazione non è corretta:

Sul lato server, quando usiamo metodi Async, dobbiamo anche aggiungere await.

Non è necessario aggiungere "await", awaitè semplicemente una comoda parola chiave in C # che consente di scrivere più righe di codice dopo la chiamata e quelle altre righe verranno eseguite solo dopo il completamento dell'operazione di salvataggio. Ma come hai sottolineato, potresti farlo semplicemente chiamando SaveChangesinvece di SaveChangesAsync.

Ma fondamentalmente, una chiamata asincrona è molto più di questo. L'idea qui è che se c'è altro lavoro che puoi fare (sul server) mentre l'operazione di salvataggio è in corso, allora dovresti usare SaveChangesAsync. Non utilizzare "wait". Chiama SaveChangesAsynce poi continua a fare altre cose in parallelo. Ciò include potenzialmente, in un'app Web, la restituzione di una risposta al client anche prima del completamento del salvataggio. Ma ovviamente, vorrai comunque controllare il risultato finale del salvataggio in modo che, nel caso in cui fallisca, puoi comunicarlo al tuo utente o registrarlo in qualche modo.


5
Si desidera effettivamente attendere queste chiamate, altrimenti è possibile eseguire query e / o salvare i dati contemporaneamente utilizzando la stessa istanza di DbContext e DbContext non è thread-safe. Inoltre, l'attesa rende facile gestire le eccezioni. Senza attesa dovresti memorizzare l'attività e controllare se è difettosa ma senza sapere quando l'attività è stata completata non sapresti quando controllare a meno che non usi '.ContinueWith' che richiede molto più pensiero che attesa.
Pawel

24
Questa risposta è ingannevole, chiamare un metodo asincrono senza attendere lo rende un "fuoco e dimentica". Il metodo si spegne e probabilmente si completerà prima o poi, ma non saprai mai quando, e se genera un'eccezione non ne sentirai mai parlare, non puoi sincronizzarti con il suo completamento. Questo tipo di comportamento potenzialmente pericoloso dovrebbe essere scelto, non invocato con una regola semplice (e non corretta) come "attende sul client, no sul server".
John Melville

1
Questa è una conoscenza molto utile che avevo letto nella documentazione, ma che non avevo davvero considerato. Quindi, hai la possibilità di: 1. SaveChangesAsync () su "Spara e dimentica", come dice John Melville ... che mi è utile in alcuni casi. 2. Attendi SaveChangesAsync () su "Fire, torna al chiamante, quindi esegui un po 'di codice di" post-salvataggio "al termine del salvataggio. Pezzo molto utile. Grazie.
Parrhesia Joe
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.