Entity Framework SaveChanges () frente a SaveChangesAsync () y Find () frente a FindAsync ()

87

He estado buscando las diferencias entre los 2 pares anteriores, pero no he encontrado ningún artículo que explique claramente al respecto ni cuándo usar uno u otro.

Entonces, ¿cuál es la diferencia entre SaveChanges()y SaveChangesAsync()?
¿Y entre Find()y FindAsync()?

En el lado del servidor, cuando usamos Asyncmétodos, también necesitamos agregar await. Por lo tanto, no creo que sea asincrónico en el lado del servidor.

¿Solo ayuda a evitar el bloqueo de la interfaz de usuario en el navegador del lado del cliente? ¿O hay pros y contras entre ellos?

Hien Tran
fuente
2
async es mucho, mucho más que evitar que el hilo de la IU del cliente se bloquee en las aplicaciones del cliente. Estoy seguro de que pronto habrá una respuesta de un experto.
jdphenix

Respuestas:

175

Cada vez que necesite realizar una acción en un servidor remoto, su programa genera la solicitud, la envía y luego espera una respuesta. Usaré SaveChanges()y SaveChangesAsync()como ejemplo, pero lo mismo se aplica a Find()yFindAsync() .

Supongamos que tiene una lista myListde más de 100 elementos que necesita agregar a su base de datos. Para insertar eso, su función se vería así:

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

Primero crea una instancia de MyEDM, agrega la lista myLista la tabla MyTable, luego llamaSaveChanges() para persistir los cambios en la base de datos. Funciona como lo desea, los registros se confirman, pero su programa no puede hacer nada más hasta que finalice la confirmación. Esto puede llevar mucho tiempo dependiendo de lo que esté cometiendo. Si está realizando cambios en los registros, la entidad tiene que confirmarlos uno a la vez (¡una vez tuve que guardar 2 minutos para las actualizaciones)!

Para resolver este problema, puede hacer una de dos cosas. La primera es que puede iniciar un nuevo hilo para manejar el inserto. Si bien esto liberará el hilo de llamada para continuar ejecutándose, creó un nuevo hilo que simplemente se quedará allí y esperará. No hay necesidad de esa sobrecarga, y esto es lo async awaitque resuelve el patrón.

Para operaciones de E / S, awaitse convierte rápidamente en su mejor amigo. Tomando la sección de código de arriba, podemos modificarla para que sea:

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

Es un cambio muy pequeño, pero tiene efectos profundos en la eficiencia y el rendimiento de su código. ¿Así que lo que pasa? El comienzo del código es el mismo, crea una instancia de MyEDMy agrega sumyList a MyTable. ¡Pero cuando llamas await context.SaveChangesAsync(), la ejecución del código vuelve a la función que llama! Entonces, mientras espera que todos esos registros se confirmen, su código puede continuar ejecutándose. Digamos que la función que contenía el código anterior tenía la firma de public async Task SaveRecords(List<MyTable> saveList), la función de llamada podría verse así:

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

Por qué tendrías una función como esta, no lo sé, pero lo que genera muestra cómo async awaitfunciona. Primero repasemos lo que sucede.

La ejecución ingresa MyCallingFunction, Function Startingluego Save Startingse escribe en la consola y luego SaveChangesAsync()se llama a la función . En este punto, la ejecución regresa MyCallingFunctione ingresa al bucle for escribiendo 'Continuar con la ejecución' hasta 1000 veces. CuandoSaveChangesAsync() finaliza, la ejecución vuelve a la SaveRecordsfunción, escribiendo Save Completeen la consola. Una vez que todo esté SaveRecordscompleto, la ejecución continuará en el lugar MyCallingFunctioncorrecto donde estaba cuando SaveChangesAsync()terminó. ¿Confuso? Aquí hay una salida de ejemplo:

Función de inicio
Guardar comenzando
¡Continuando ejecutando!
¡Continuando ejecutando!
¡Continuando ejecutando!
¡Continuando ejecutando!
¡Continuando ejecutando!
....
¡Continuando ejecutando!
¡Guardar completo!
¡Continuando ejecutando!
¡Continuando ejecutando!
¡Continuando ejecutando!
....
¡Continuando ejecutando!
¡Función completada!

O tal vez:

Función de inicio
Guardar comenzando
¡Continuando ejecutando!
¡Continuando ejecutando!
¡Guardar completo!
¡Continuando ejecutando!
¡Continuando ejecutando!
¡Continuando ejecutando!
....
¡Continuando ejecutando!
¡Función completada!

Esa es la belleza de async awaitsu código puede continuar ejecutándose mientras espera que algo termine. En realidad, tendrías una función más como esta como tu función de llamada:

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

Aquí, tiene cuatro funciones diferentes de guardar registros al mismo tiempo . MyCallingFunctionse completará mucho más rápido usando async awaitque si las SaveRecordsfunciones individuales fueran llamadas en serie.

Lo único que aún no he mencionado es la awaitpalabra clave. Lo que hace esto es detener la ejecución de la función actual hasta Taskque se complete lo que está esperando. Entonces, en el caso del original MyCallingFunction, la línea Function Completeno se escribirá en la consola hasta SaveRecordsque finalice la función.

En pocas palabras, si tiene una opción para usar async await, debería hacerlo, ya que aumentará en gran medida el rendimiento de su aplicación.

Jacob Lambert
fuente
7
El 99% del tiempo todavía tengo que esperar a que se reciban los valores de la base de datos antes de poder continuar. ¿Debería seguir usando async? ¿Async permite que 100 personas se conecten a mi sitio web de forma asincrónica? Si no uso async, ¿significa eso que los 100 usuarios tienen que esperar en la línea 1 a la vez?
MIKE
6
Vale la pena señalar: generar un nuevo hilo del grupo de hilos convierte a ASP en un panda triste, ya que básicamente robas un hilo de ASP (lo que significa que el hilo no puede manejar otras solicitudes ni hacer nada en absoluto, ya que está atascado en una llamada de bloqueo). awaitSin embargo, si usa , incluso si USTED no necesita hacer nada más después de la llamada a SaveChanges, ASP dirá "aha, este hilo regresó esperando una operación asíncrona, esto significa que puedo dejar que este hilo maneje alguna otra solicitud mientras tanto ! " Esto hace que su aplicación escale horizontalmente mucho mejor.
Sara
3
En realidad, he comparado el async para que sea más lento. ¿Y alguna vez ha visto cuántos subprocesos están disponibles en un servidor ASP.Net típico? Son como decenas de miles. Entonces, las probabilidades de quedarse sin subprocesos para manejar más solicitudes son muy poco probables e incluso si tuviera suficiente tráfico para saturar todos esos subprocesos, ¿su servidor es realmente lo suficientemente poderoso como para no fallar en ese caso de todos modos? Afirmar que el uso de async en todas partes aumenta el rendimiento es totalmente incorrecto. Puede hacerlo en ciertos escenarios, pero en la mayoría de las situaciones habituales será más lento. Compare y vea.
user3766657
@MIKE, mientras que un solo usuario debe esperar a que la base de datos devuelva datos para continuar, los otros usuarios que usan su aplicación no lo hacen. Si bien IIS crea un hilo para cada solicitud (en realidad es más complejo que eso), su hilo en espera se puede utilizar para manejar otras solicitudes, esto es importante para la escalabilidad afaik. Generando imágenes de cada solicitud, en lugar de usar 1 subproceso a tiempo completo, utiliza muchos subprocesos más cortos que se pueden reutilizar en otro lugar (también conocido como otras solicitudes).
Bart Calixto
1
Me gustaría simplemente añadir que siempre debe await de SaveChangesAsyncpuesto EF no admite varios ahorra al mismo tiempo. docs.microsoft.com/en-us/ef/core/saving/async Además, existe una gran ventaja al utilizar estos métodos asíncronos. Por ejemplo, puede seguir recibiendo otras solicitudes en su webApi al guardar datos o realizar muchas tareas, o mejorar la experiencia del usuario sin congelar la interfaz cuando está en una aplicación de escritorio.
tgarcia
1

Mi explicación restante se basará en el siguiente fragmento de código.

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

ingrese la descripción de la imagen aquí

Observaciones: Como la parte sincrónica (verde) de JobAsync giros más largos que la tarea t(rojo), la tarea tya está completada en el punto de await t. Como resultado, la continuación (azul) se ejecuta en el mismo hilo que el verde. La parte sincrónica de Main(blanco) girará después de que la verde termine de girar. Es por eso que la parte sincrónica en el método asincrónico es problemática.

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

ingrese la descripción de la imagen aquí

Observaciones: Este caso es opuesto al primer caso. La parte sincrónica (verde) de los JobAsyncgiros más cortos que la tarea.t (rojo), entonces la tarea tno se ha completado en el punto de await t. Como resultado, la continuación (azul) se ejecuta en el hilo diferente al verde. La parte sincrónica de Main(blanco) todavía gira después de que la verde termina de girar.

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

ingrese la descripción de la imagen aquí

Observaciones: Este caso solucionará el problema de los casos anteriores sobre la parte síncrona en método asíncrono. La tareat se espera de inmediato. Como resultado, la continuación (azul) se ejecuta en el hilo diferente al verde. La parte sincrónica de Main(blanco) girará inmediatamente en paralelo a JobAsync.

Si desea agregar otros casos, no dude en editar.

Programador orientado al dinero
fuente
1

Esta declaración es incorrecta:

En el lado del servidor, cuando usamos métodos Async, también necesitamos agregar await.

No es necesario agregar "await", awaites simplemente una palabra clave conveniente en C # que le permite escribir más líneas de código después de la llamada, y esas otras líneas solo se ejecutarán después de que se complete la operación Guardar. Pero, como señaló, podría lograrlo simplemente llamando en SaveChangeslugar deSaveChangesAsync .

Pero, fundamentalmente, una llamada asincrónica es mucho más que eso. La idea aquí es que si hay otro trabajo que pueda hacer (en el servidor) mientras la operación Guardar está en progreso, entonces debería usar SaveChangesAsync. No utilice "aguardar". Simplemente llame SaveChangesAsyncy luego continúe haciendo otras cosas en paralelo. Esto incluye potencialmente, en una aplicación web, devolver una respuesta al cliente incluso antes de que se haya completado el guardado. Pero, por supuesto, aún querrá verificar el resultado final de Guardar para que, en caso de que falle, pueda comunicárselo a su usuario o iniciar sesión de alguna manera.

Rajeev Goel
fuente
4
En realidad, desea esperar estas llamadas, de lo contrario, podría ejecutar consultas o guardar datos al mismo tiempo utilizando la misma instancia de DbContext y DbContext no es seguro para subprocesos. Además, await facilita el manejo de excepciones. Sin esperar, tendría que almacenar la tarea y verificar si tiene fallas, pero sin saber cuándo se completó la tarea, no sabría cuándo verificar a menos que use '.ContinueWith', que requiere mucho más pensamiento que esperar.
Pawel
22
Esta respuesta es engañosa. Llamar a un método asincrónico sin esperar lo convierte en un "disparar y olvidar". El método se activa y probablemente se completará en algún momento, pero nunca sabrá cuándo, y si arroja una excepción, nunca se enterará. No puede sincronizar con su finalización. Se debe elegir este tipo de comportamiento potencialmente peligroso, no invocarlo con una regla simple (e incorrecta) como "espera en el cliente, no espera en el servidor".
John Melville
1
Este es un conocimiento muy útil que había leído en la documentación, pero que realmente no había considerado. Por lo tanto, tiene la opción de: 1. SaveChangesAsync () a "Activar y olvidar", como dice John Melville ... lo cual es útil para mí en algunos casos. 2. Espere SaveChangesAsync () a "Disparar, regresar a la persona que llama y luego ejecutar un código 'posterior al guardado' después de que se complete el guardado. Pieza muy útil. Gracias.
Parrhesia Joe