Entity Framework SaveChanges () vs. SaveChangesAsync () und Find () vs. FindAsync ()

85

Ich habe nach den Unterschieden zwischen 2 Paaren oben gesucht, aber keine Artikel gefunden, die klar erklären, wann und wann ich das eine oder andere verwenden soll.

Was ist der Unterschied zwischen SaveChanges()und SaveChangesAsync()?
Und zwischen Find()und FindAsync()?

Auf der Serverseite müssen wir bei der Verwendung von AsyncMethoden auch hinzufügen await. Daher denke ich nicht, dass es auf der Serverseite asynchron ist.

Hilft es nur, die Blockierung der Benutzeroberfläche im clientseitigen Browser zu verhindern? Oder gibt es Vor- und Nachteile zwischen ihnen?

Hien Tran
quelle
2
Async ist viel, viel mehr als das Blockieren des Client-UI-Threads in Client-Anwendungen. Ich bin mir sicher, dass in Kürze eine Expertenantwort kommt.
jdphenix

Antworten:

168

Jedes Mal, wenn Sie eine Aktion auf einem Remote-Server ausführen müssen, generiert Ihr Programm die Anforderung, sendet sie und wartet auf eine Antwort. Ich werde SaveChanges()und SaveChangesAsync()als Beispiel verwenden, aber das gleiche gilt für Find()und FindAsync().

Angenommen, Sie haben eine Liste myListmit mehr als 100 Elementen, die Sie Ihrer Datenbank hinzufügen müssen. Um das einzufügen, würde Ihre Funktion ungefähr so ​​aussehen:

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

Zuerst erstellen Sie eine Instanz von MyEDM, fügen die Liste myListder Tabelle hinzu MyTableund rufen dann auf, SaveChanges()um die Änderungen an der Datenbank beizubehalten. Es funktioniert wie Sie möchten, die Datensätze werden festgeschrieben, aber Ihr Programm kann nichts anderes tun, bis das Festschreiben abgeschlossen ist. Dies kann lange dauern, je nachdem, was Sie festlegen. Wenn Sie Änderungen an den Datensätzen vornehmen, muss die Entität diese nacheinander festschreiben (ich hatte einmal 2 Minuten Zeit für Aktualisierungen, um sie zu speichern)!

Um dieses Problem zu lösen, können Sie eines von zwei Dingen tun. Das erste ist, dass Sie einen neuen Thread starten können, um die Einfügung zu handhaben. Dadurch wird der aufrufende Thread für die weitere Ausführung freigegeben. Sie haben jedoch einen neuen Thread erstellt, der nur dort sitzt und wartet. Es gibt keine Notwendigkeit für diesen Aufwand, und das ist, was dieasync await löst Muster.

Für E / A-Operationen wird awaitschnell Ihr bester Freund. Wenn wir den Codeabschnitt von oben nehmen, können wir ihn wie folgt ändern:

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

Es ist eine sehr kleine Änderung, aber es gibt tiefgreifende Auswirkungen auf die Effizienz und Leistung Ihres Codes. Was passiert also? Der Beginn des Codes ist das gleiche, Sie eine Instanz erstellen MyEDMund fügen Sie Ihre myListzu MyTable. Wenn Sie jedoch aufrufen await context.SaveChangesAsync(), kehrt die Ausführung des Codes zur aufrufenden Funktion zurück!Während Sie darauf warten, dass alle diese Datensätze festgeschrieben werden, kann Ihr Code weiterhin ausgeführt werden. Angenommen, die Funktion, die den obigen Code enthielt, hatte die Signatur public async Task SaveRecords(List<MyTable> saveList), die aufrufende Funktion könnte folgendermaßen aussehen:

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

Warum Sie eine solche Funktion haben würden, weiß ich nicht, aber was sie ausgibt, zeigt, wie es async awaitfunktioniert. Lassen Sie uns zuerst untersuchen, was passiert.

Die Ausführung wird eingegeben MyCallingFunction, Function Startingdann Save Startingin die Konsole geschrieben und die Funktion SaveChangesAsync()aufgerufen. Zu diesem Zeitpunkt kehrt die Ausführung zu MyCallingFunctionder for-Schleife zurück und gibt sie bis zu 1000 Mal ein. Wenn der SaveChangesAsync()Vorgang abgeschlossen ist, kehrt die Ausführung zur SaveRecordsFunktion zurück und schreibt Save Completein die Konsole. Sobald alles SaveRecordsabgeschlossen ist, wird die Ausführung in fortgesetztMyCallingFunction genau dort sie abgeschlossen war SaveChangesAsync(). Verwirrt? Hier ist eine Beispielausgabe:

Funktionsstart
Speichern Start
Weiter ausführen!
Weiter ausführen!
Weiter ausführen!
Weiter ausführen!
Weiter ausführen!
....
Weiter ausführen!
Speichern Sie vollständig!
Weiter ausführen!
Weiter ausführen!
Weiter ausführen!
....
Weiter ausführen!
Funktion abgeschlossen!

Oder vielleicht:

Funktionsstart
Speichern Start
Weiter ausführen!
Weiter ausführen!
Speichern Sie vollständig!
Weiter ausführen!
Weiter ausführen!
Weiter ausführen!
....
Weiter ausführen!
Funktion abgeschlossen!

Das ist das Schöne daran: async awaitIhr Code kann weiterhin ausgeführt werden, während Sie darauf warten, dass etwas beendet wird. In Wirklichkeit hätten Sie eine ähnliche Funktion als Ihre aufrufende Funktion:

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

Hier haben Sie vier verschiedene Funktionen zum Speichern von Datensätzen gleichzeitig . MyCallingFunctionwird viel schneller ausgeführt, async awaitals wenn die einzelnen SaveRecordsFunktionen in Reihe aufgerufen würden.

Das einzige, was ich noch nicht angesprochen habe, ist das awaitSchlüsselwort. Dadurch wird die Ausführung der aktuellen Funktion gestoppt, bis alles Task, was Sie erwarten, abgeschlossen ist. Im Fall des Originals MyCallingFunctionwird die Zeile Function Completealso erst nach Abschluss der SaveRecordsFunktion in die Konsole geschrieben .

Kurz gesagt, wenn Sie eine Option zur Verwendung haben async await, sollten Sie dies tun, da dies die Leistung Ihrer Anwendung erheblich steigern wird.

Jacob Lambert
quelle
7
In 99% der Fälle muss ich noch warten, bis Werte aus der Datenbank empfangen werden, bevor ich fortfahren kann. Sollte ich immer noch Async verwenden? Ermöglicht Async 100 Personen die asynchrone Verbindung zu meiner Website? Wenn ich kein Async verwende, müssen dann alle 100 Benutzer gleichzeitig in Zeile 1 warten?
Mike
6
Bemerkenswert: Das Laichen eines neuen Threads aus dem Thread-Pool macht ASP zu einem traurigen Panda, da Sie im Grunde genommen einen Thread aus ASP rauben (was bedeutet, dass der Thread keine anderen Anforderungen verarbeiten oder überhaupt nichts tun kann, da er in einem blockierenden Aufruf steckt). Wenn Sie awaitjedoch verwenden, auch wenn SIE nach dem Aufruf von SaveChanges nichts anderes tun müssen, sagt ASP "aha, dieser Thread wurde zurückgegeben und wartet auf eine asynchrone Operation. Dies bedeutet, dass ich diesen Thread in der Zwischenzeit eine andere Anfrage bearbeiten lassen kann ! " Dadurch wird Ihre App horizontal viel besser skaliert.
Sara
3
Eigentlich habe ich Async als langsamer eingestuft. Und haben Sie jemals gesehen, wie viele Threads in einem typischen ASP.Net-Server verfügbar sind? Es ist wie Zehntausende. Es ist also sehr unwahrscheinlich, dass die Threads ausgehen, um weitere Anforderungen zu bearbeiten. Ist Ihr Server wirklich leistungsfähig genug, um in diesem Fall nicht zu knicken, selbst wenn Sie über genügend Datenverkehr verfügen, um alle diese Threads zu überlasten? Die Behauptung, dass die Verwendung von Async überall die Leistung steigert, ist völlig falsch. Es kann in bestimmten Szenarien sein, aber in den meisten Situationen wird es tatsächlich langsamer sein. Benchmark und sehen.
user3766657
@MIKE Während ein einzelner Benutzer warten muss, bis die Datenbank Daten zurückgibt, um fortzufahren, tun dies Ihre anderen Benutzer, die Ihre Anwendung verwenden, nicht. Während IIS für jede Anforderung einen Thread erstellt (tatsächlich ist er komplexer als dieser), kann Ihr wartender Thread zur Verarbeitung anderer Anforderungen verwendet werden. Dies ist aus Gründen der Skalierbarkeit wichtig. Imaging jeder Anforderung, anstatt 1 Thread Vollzeit zu verwenden, werden viele kürzere Threads verwendet, die an einer anderen Stelle wiederverwendet werden können (auch bekannt als andere Anforderungen).
Bart Calixto
1
Ich möchte nur hinzufügen , dass Sie sollten immer await für SaveChangesAsyncda EF nicht mehrere gleichzeitig spart nicht unterstützt. docs.microsoft.com/en-us/ef/core/saving/async Außerdem bietet die Verwendung dieser asynchronen Methoden einen großen Vorteil. Sie können beispielsweise weiterhin andere Anforderungen in Ihrem webApi empfangen, wenn Sie Daten speichern oder viel Sutuff ausführen, oder die Benutzererfahrung verbessern, ohne die Benutzeroberfläche einzufrieren, wenn Sie sich in einer Desktop-Anwendung befinden.
Tgarcia
1

Meine verbleibende Erklärung basiert auf dem folgenden Codefragment.

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

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

Geben Sie hier die Bildbeschreibung ein

Anmerkungen: Da sich der synchrone Teil (grün) von JobAsyncDrehungen länger als die Aufgabe t(rot) dreht, ist die Aufgabe tbereits zum Zeitpunkt abgeschlossenawait t . Infolgedessen läuft die Fortsetzung (blau) auf demselben Thread wie die grüne. Der synchrone Teil von Main(weiß) dreht sich, nachdem der grüne Teil fertig ist. Aus diesem Grund ist der synchrone Teil bei der asynchronen Methode problematisch.

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

Geben Sie hier die Bildbeschreibung ein

Anmerkungen: Dieser Fall ist dem ersten Fall entgegengesetzt. Der synchrone Teil (grün) der JobAsyncDrehungen, der kürzer als die Aufgabe t(rot) ist, dann ist die Aufgabe tzum Zeitpunkt von noch nicht abgeschlossenawait t . Infolgedessen läuft die Fortsetzung (blau) auf dem anderen Thread als der grüne. Der synchrone Teil von Main(weiß) dreht sich immer noch, nachdem der grüne Teil fertig ist.

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

Geben Sie hier die Bildbeschreibung ein

Anmerkungen: Dieser Fall löst das Problem in den vorherigen Fällen bezüglich des synchronen Teils in der asynchronen Methode. Die Aufgabe twird sofort erwartet. Infolgedessen läuft die Fortsetzung (blau) auf dem anderen Thread als der grüne. Der synchrone Teil vonMain (weiß) dreht sich sofort parallel zu JobAsync.

Wenn Sie weitere Fälle hinzufügen möchten, können Sie diese jederzeit bearbeiten.

Wissen Macht Frei
quelle
1

Diese Aussage ist falsch:

Auf der Serverseite müssen wir bei Verwendung von Async-Methoden auch wait hinzufügen.

Sie müssen nicht "Warten" hinzufügen. Dies awaitist lediglich ein praktisches Schlüsselwort in C #, mit dem Sie nach dem Aufruf weitere Codezeilen schreiben können. Diese anderen Zeilen werden erst ausgeführt, nachdem der Speichervorgang abgeschlossen ist. Aber wie Sie bereits betont haben, können Sie dies einfach durch einen Anruf erreichenSaveChanges stattSaveChangesAsync .

Grundsätzlich geht es bei einem asynchronen Aufruf jedoch um viel mehr. Die Idee hier ist, dass Sie verwenden sollten, wenn Sie andere Arbeiten (auf dem Server) ausführen können, während der Speichervorgang ausgeführt wird SaveChangesAsync. Verwenden Sie nicht "Warten". Rufen Sie einfach an SaveChangesAsyncund machen Sie dann parallel andere Dinge weiter. Dies umfasst möglicherweise die Rückgabe einer Antwort an den Client in einer Web-App, noch bevor der Speichervorgang abgeschlossen ist. Aber natürlich möchten Sie immer noch das Endergebnis des Speicherns überprüfen, damit Sie es Ihrem Benutzer mitteilen oder es irgendwie protokollieren können, falls es fehlschlägt.

Rajeev Goel
quelle
4
Sie möchten diese Aufrufe tatsächlich abwarten, da Sie sonst möglicherweise Abfragen ausführen und / oder Daten gleichzeitig mit derselben DbContext-Instanz speichern und DbContext nicht threadsicher ist. Darüber hinaus erleichtert das Warten das Behandeln von Ausnahmen. Ohne Warten müssten Sie die Aufgabe speichern und prüfen, ob sie fehlerhaft ist, aber ohne zu wissen, wann die Aufgabe abgeschlossen ist, würden Sie nicht wissen, wann Sie prüfen müssen, es sei denn, Sie verwenden '.ContinueWith', was viel mehr Gedanken als Warten erfordert.
Pawel
22
Diese Antwort täuscht. Wenn Sie eine asynchrone Methode ohne Wartezeit aufrufen, wird sie zu einem "Feuer und Vergessen". Die Methode wird deaktiviert und wahrscheinlich irgendwann abgeschlossen, aber Sie werden nie wissen, wann und wenn eine Ausnahme ausgelöst wird, werden Sie nie davon erfahren. Sie können nicht mit dem Abschluss synchronisieren. Diese Art von potenziell gefährlichem Verhalten sollte ausgewählt und nicht mit einer einfachen (und falschen) Regel wie "Warten auf dem Client, keine Warten auf dem Server" aufgerufen werden.
John Melville
1
Dies ist ein sehr nützliches Wissen, das ich in der Dokumentation gelesen, aber nicht wirklich berücksichtigt hatte. Sie haben also die Möglichkeit: 1. SaveChangesAsync () auf "Feuer und Vergessen" zu speichern, wie John Melville sagt ... was in einigen Fällen für mich nützlich ist. 2. Warten Sie, bis SaveChangesAsync () "Fire" ist, kehren Sie zum Aufrufer zurück und führen Sie nach Abschluss des Speichervorgangs einen Code nach dem Speichern aus. Sehr nützliches Stück. Vielen Dank.
Parrhesia Joe,