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

87

Szukałem różnic między 2 parami powyżej, ale nie znalazłem żadnych artykułów wyjaśniających to jasno, a także kiedy użyć jednej lub drugiej.

Jaka jest różnica między SaveChanges()i SaveChangesAsync()?
A między Find()a FindAsync()?

Po stronie serwera, gdy używamy Asyncmetod, musimy również dodać await. Dlatego nie sądzę, że jest asynchroniczny po stronie serwera.

Czy pomaga to tylko w zapobieganiu blokowaniu interfejsu użytkownika w przeglądarce po stronie klienta? A może są między nimi jakieś wady i zalety?

Hien Tran
źródło
2
asynchroniczny jest dużo, dużo więcej niż zatrzymanie wątku UI klienta blokowaniu w aplikacjach klienckich. Jestem pewien, że wkrótce pojawi się odpowiedź eksperta.
jdphenix

Odpowiedzi:

175

Za każdym razem, gdy musisz wykonać jakąś akcję na serwerze zdalnym, twój program generuje żądanie, wysyła je, a następnie czeka na odpowiedź. Użyję SaveChanges()i SaveChangesAsync()jako przykładu, ale to samo dotyczy Find()i FindAsync().

Załóżmy, że masz listę myListponad 100 elementów, które musisz dodać do swojej bazy danych. Aby to wstawić, twoja funkcja wyglądałaby mniej więcej tak:

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

Najpierw tworzysz instancję MyEDM, dodajesz listę myListdo tabeli MyTable, a następnie wywołujesz, SaveChanges()aby utrwalić zmiany w bazie danych. Działa tak, jak chcesz, rekordy są zatwierdzane, ale twój program nie może zrobić nic innego, dopóki zatwierdzanie nie zostanie zakończone. Może to zająć dużo czasu w zależności od tego, co robisz. Jeśli zatwierdzasz zmiany w rekordach, podmiot musi zatwierdzać je pojedynczo (kiedyś miałem zapis, który zajął 2 minuty na aktualizacje)!

Aby rozwiązać ten problem, możesz zrobić jedną z dwóch rzeczy. Po pierwsze, możesz uruchomić nowy gwint do obsługi wkładki. Chociaż zwolni to wątek wywołujący, aby kontynuować wykonywanie, utworzyłeś nowy wątek, który będzie tam siedział i czekał. Nie ma potrzeby tego narzutu i to właśnie async awaitrozwiązuje wzór.

W przypadku operacji we / wy awaitszybko staje się najlepszym przyjacielem. Biorąc sekcję kodu z góry, możemy zmodyfikować ją tak, aby była:

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

To bardzo mała zmiana, ale ma głęboki wpływ na wydajność i wydajność Twojego kodu. Więc co się dzieje? Początek kodu jest taki sam, tworzysz instancję MyEDMi dodajesz ją myListdo MyTable. Ale kiedy dzwonisz await context.SaveChangesAsync(), wykonanie kodu wraca do funkcji wywołującej! Więc gdy czekasz na zatwierdzenie wszystkich tych rekordów, twój kod może nadal być wykonywany. Powiedzmy, że funkcja, która zawiera powyższy kod ma podpis public async Task SaveRecords(List<MyTable> saveList), funkcja wywołująca mogłaby wyglądać tak:

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

Dlaczego miałbyś mieć taką funkcję, nie wiem, ale to, co wyprowadza, pokazuje, jak async awaitdziała. Najpierw przyjrzyjmy się, co się dzieje.

Wykonywanie wchodzi MyCallingFunction, Function Startingnastępnie Save Startingzostaje zapisane w konsoli, a następnie SaveChangesAsync()wywoływana jest funkcja . W tym momencie wykonanie powraca MyCallingFunctioni wchodzi do pętli for, zapisując „Kontynuacja wykonywania” do 1000 razy. Po SaveChangesAsync()zakończeniu wykonanie wraca do SaveRecordsfunkcji, pisząc Save Completedo konsoli. Gdy wszystko zostanie SaveRecordsukończone, wykonywanie będzie kontynuowane MyCallingFunctiontak, jak było po SaveChangesAsync()zakończeniu. Zmieszany? Oto przykładowe dane wyjściowe:

Uruchamianie funkcji
Zapisz Rozpoczynanie
Kontynuacja wykonywania!
Kontynuacja wykonywania!
Kontynuacja wykonywania!
Kontynuacja wykonywania!
Kontynuacja wykonywania!
....
Kontynuacja wykonywania!
Zapisz zakończone!
Kontynuacja wykonywania!
Kontynuacja wykonywania!
Kontynuacja wykonywania!
....
Kontynuacja wykonywania!
Funkcja zakończona!

Albo może:

Uruchamianie funkcji
Zapisz Rozpoczynanie
Kontynuacja wykonywania!
Kontynuacja wykonywania!
Zapisz zakończone!
Kontynuacja wykonywania!
Kontynuacja wykonywania!
Kontynuacja wykonywania!
....
Kontynuacja wykonywania!
Funkcja zakończona!

Na tym polega piękno async await, Twój kod może nadal działać, gdy czekasz na zakończenie. W rzeczywistości jako funkcję wywołującą miałbyś funkcję podobną do tej:

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

Tutaj masz cztery różne funkcje zapisu danych działające w tym samym czasie . MyCallingFunctionzakończy się dużo szybciej, async awaitniż gdyby poszczególne SaveRecordsfunkcje były wywoływane szeregowo.

Jedyną rzeczą, której jeszcze nie poruszyłem, jest awaitsłowo kluczowe. Powoduje to zatrzymanie wykonywania bieżącej funkcji do czasu Taskzakończenia tego , na co czekasz. Tak więc w przypadku oryginału MyCallingFunctionlinia Function Completenie zostanie zapisana na konsoli, dopóki SaveRecordsfunkcja nie zostanie zakończona.

Krótko mówiąc, jeśli masz możliwość użycia async await, powinieneś, ponieważ znacznie zwiększy to wydajność Twojej aplikacji.

Jacob Lambert
źródło
7
W 99% przypadków nadal muszę czekać na odebranie wartości z bazy danych, zanim będę mógł kontynuować. Czy nadal powinienem używać async? Czy async pozwala 100 osobom na asynchroniczne łączenie się z moją witryną? Jeśli nie używam async, czy to oznacza, że ​​wszystkich 100 użytkowników musi czekać w linii 1 na raz?
MIKE,
6
Warto zauważyć: Spawnowanie nowego wątku z puli wątków sprawia, że ​​ASP jest smutną pandą, ponieważ w zasadzie okradasz wątek z ASP (co oznacza, że ​​wątek nie może obsłużyć innych żądań ani nic nie robić, ponieważ utknął w wywołaniu blokującym). Jeśli awaitjednak używasz , nawet jeśli nie musisz robić nic więcej po wywołaniu SaveChanges, ASP powie "aha, ten wątek powrócił w oczekiwaniu na operację asynchroniczną, co oznacza, że ​​mogę pozwolić temu wątkowi obsłużyć w międzyczasie inne żądanie ! ” Dzięki temu Twoja aplikacja znacznie lepiej skaluje się w poziomie.
sara
3
Właściwie testowałem asynchroniczność, aby była wolniejsza. Czy kiedykolwiek widziałeś, ile wątków jest dostępnych na typowym serwerze ASP.Net? To jak dziesiątki tysięcy. Więc prawdopodobieństwo, że zabraknie wątków do obsługi dalszych żądań jest bardzo mało prawdopodobne, a nawet jeśli miałeś wystarczająco dużo ruchu, aby nasycić wszystkie te wątki, czy Twój serwer jest naprawdę wystarczająco wydajny, aby i tak nie zapadać się w tym przypadku? Twierdzenie, że używanie async wszędzie zwiększa wydajność, jest całkowicie błędne. Może w pewnych sytuacjach, ale w większości sytuacji będzie wolniejszy. Testuj i zobacz.
user3766657
@MIKE, podczas gdy pojedynczy użytkownik musi czekać, aż baza danych zwróci dane, aby kontynuować, inni użytkownicy używający aplikacji nie. Chociaż usługi IIS tworzą wątek dla każdego żądania (w rzeczywistości jest to bardziej skomplikowane), oczekujący wątek może być używany do obsługi innych żądań, jest to ważne dla skalowalności afaik. Tworząc obraz każdego żądania, zamiast używać 1 wątku w pełnym wymiarze czasu, wykorzystuje wiele krótszych wątków, które można ponownie wykorzystać w innym miejscu (czyli inne żądania).
Bart Calixto
1
Chciałbym tylko dodać, że powinieneś zawsze await for, SaveChangesAsyncponieważ EF nie obsługuje wielu zapisów w tym samym czasie. docs.microsoft.com/en-us/ef/core/saving/async Ponadto korzystanie z tych metod asynchronicznych ma naprawdę dużą zaletę. Na przykład możesz nadal otrzymywać inne żądania w swoim webApi podczas zapisywania danych lub wykonywania wielu operacji sutuff, lub poprawić wrażenia użytkownika, nie blokując interfejsu, gdy jesteś w aplikacji komputerowej.
tgarcia
1

Moje pozostałe wyjaśnienie będzie oparte na następującym fragmencie kodu.

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

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

wprowadź opis obrazu tutaj

Uwagi: Ponieważ synchroniczna część (zielona) JobAsyncwiruje dłużej niż zadanie t(kolor czerwony), to zadanie tjest już zakończone w punkcie await t. W rezultacie kontynuacja (niebieska) przebiega na tej samej nitce co zielona. Synchroniczna część Main(biała) obróci się, gdy zielona skończy się kręcić. Dlatego część synchroniczna w metodzie asynchronicznej jest problematyczna.

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

wprowadź opis obrazu tutaj

Uwagi: Ten przypadek jest odwrotny do pierwszego przypadku. Synchroniczna część (kolor zielony) JobAsynckręci się krócej niż zadanie t(kolor czerwony) to zadanie tnie zostało ukończone w punkcie await t. W rezultacie kontynuacja (niebieska) przebiega na innym wątku niż zielony. Synchroniczna część Main(biała) nadal obraca się po tym, jak zielona kończy się obracać.

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

wprowadź opis obrazu tutaj

Uwagi: Ten przypadek rozwiąże problem z poprzednich przypadków dotyczący części synchronicznej w metodzie asynchronicznej. Zadanie tjest natychmiast oczekiwane. W rezultacie kontynuacja (niebieska) przebiega na innym wątku niż zielony. Synchroniczna część Main(biała) obróci się natychmiast równolegle do JobAsync.

Jeśli chcesz dodać inne sprawy, możesz je edytować.

Programista zorientowany na pieniądze
źródło
1

To stwierdzenie jest nieprawidłowe:

Po stronie serwera, gdy używamy metod Async, musimy również dodać await.

Nie trzeba dodawać „await”, awaitjest to po prostu wygodne słowo kluczowe w języku C #, które umożliwia napisanie większej liczby wierszy kodu po wywołaniu, a pozostałe wiersze zostaną wykonane dopiero po zakończeniu operacji zapisywania. Ale jak zauważyłeś, możesz to osiągnąć po prostu dzwoniąc SaveChangeszamiast SaveChangesAsync.

Ale zasadniczo wywołanie asynchroniczne to znacznie więcej. Pomysł jest taki, że jeśli jest inna praca, którą możesz wykonać (na serwerze) w trakcie operacji zapisywania, powinieneś użyć SaveChangesAsync. Nie używaj „await”. Po prostu zadzwoń SaveChangesAsync, a następnie kontynuuj równolegle wykonywanie innych czynności. Obejmuje to potencjalnie, w aplikacji internetowej, zwrócenie odpowiedzi do klienta, nawet przed zakończeniem zapisywania. Ale oczywiście nadal będziesz chciał sprawdzić ostateczny wynik zapisywania, aby w przypadku niepowodzenia możesz przekazać to użytkownikowi lub w jakiś sposób zarejestrować.

Rajeev Goel
źródło
4
W rzeczywistości chcesz poczekać na te wywołania, w przeciwnym razie możesz uruchamiać zapytania i / lub zapisywać dane jednocześnie przy użyciu tego samego wystąpienia DbContext, a DbContext nie jest bezpieczny wątkowo. Co więcej, await ułatwia obsługę wyjątków. Bez czekania musiałbyś zapisać zadanie i sprawdzić, czy jest błędne, ale nie wiedząc, kiedy zadanie zostało ukończone, nie wiedziałbyś, kiedy to sprawdzić, chyba że użyjesz „.ContinueWith”, co wymaga znacznie więcej przemyśleń niż czekania.
Paweł
22
Ta odpowiedź jest zwodnicza. Wywołanie metody asynchronicznej bez czekania powoduje, że jest to „odpal i zapomnij”. Metoda znika i prawdopodobnie kiedyś się zakończy, ale nigdy nie będziesz wiedział kiedy, a jeśli rzuci wyjątek, to nigdy o nim nie usłyszysz, nie możesz zsynchronizować się z jej zakończeniem. Tego rodzaju potencjalnie niebezpieczne zachowanie powinno być wybierane, a nie wywoływane prostą (i niepoprawną) regułą, taką jak „oczekuje na klienta, nie ma oczekiwania na serwerze”.
John Melville
1
Jest to bardzo przydatna wiedza, którą przeczytałem w dokumentacji, ale tak naprawdę jej nie rozważałem. Masz więc możliwość: 1. SaveChangesAsync () do "Uruchom i zapomnij", jak mówi John Melville ... co jest dla mnie przydatne w niektórych przypadkach. 2. poczekaj SaveChangesAsync () na „Uruchom, wróć do wywołującego, a następnie wykonaj kod„ po zapisaniu ”po zakończeniu zapisywania. Bardzo przydatny kawałek. Dziękuję.
Parrhesia Joe