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


86

Tenho pesquisado as diferenças entre os 2 pares acima, mas não encontrei nenhum artigo explicando claramente sobre isso, bem como quando usar um ou outro.

Então, qual é a diferença entre SaveChanges()e SaveChangesAsync()?
E entre Find()e FindAsync()?

No lado do servidor, quando usamos Asyncmétodos, também precisamos adicionar await. Portanto, não acho que seja assíncrono no lado do servidor.

Isso só ajuda a evitar o bloqueio da IU no navegador do lado do cliente? Ou existem prós e contras entre eles?


2
async é muito, muito mais do que impedir que o encadeamento da IU do cliente bloqueie em aplicativos cliente. Tenho certeza de que haverá uma resposta de especialista em breve.
jdphenix

Respostas:


174

Sempre que você precisar realizar uma ação em um servidor remoto, seu programa gera a solicitação, a envia e espera por uma resposta. Vou usar SaveChanges()e SaveChangesAsync()como exemplo, mas o mesmo se aplica a Find()e FindAsync().

Digamos que você tenha uma lista myListde mais de 100 itens que precise adicionar ao seu banco de dados. Para inserir isso, sua função seria mais ou menos assim:

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

Primeiro você cria uma instância de MyEDM, adiciona a lista myListà tabela e MyTable, em seguida, chama SaveChanges()para persistir as alterações no banco de dados. Funciona como você deseja, os registros são confirmados, mas seu programa não pode fazer mais nada até que a confirmação seja concluída. Isso pode levar muito tempo, dependendo do que você está enviando. Se você está enviando alterações para os registros, a entidade tem que confirmá-las, uma de cada vez (uma vez, um salvamento levou 2 minutos para atualizações)!

Para resolver esse problema, você pode fazer uma das duas coisas. A primeira é que você pode iniciar uma nova rosca para lidar com a inserção. Embora isso libere o encadeamento de chamada para continuar em execução, você criou um novo encadeamento que vai ficar parado e esperar. Não há necessidade dessa sobrecarga, e é isso que o async awaitpadrão resolve.

Para operações de I / O, awaitrapidamente se torna seu melhor amigo. Pegando a seção de código acima, podemos modificá-la para ser:

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

É uma mudança muito pequena, mas há efeitos profundos na eficiência e no desempenho do seu código. Então o que acontece? O início do código é o mesmo, você cria uma instância de MyEDMe adiciona seu myLista MyTable. Mas quando você chama await context.SaveChangesAsync(), a execução do código retorna para a função de chamada! Portanto, enquanto você espera a confirmação de todos esses registros, seu código pode continuar a ser executado. Digamos que a função que continha o código acima tivesse a assinatura de public async Task SaveRecords(List<MyTable> saveList), a função de chamada poderia ter a seguinte aparência:

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 que você teria uma função como esta, eu não sei, mas o que sai mostra como async awaitfunciona. Primeiro, vamos ver o que acontece.

Execução entra MyCallingFunction, Function Startingem seguida, Save Startingé escrita para o console, em seguida, a função SaveChangesAsync()é chamada. Neste ponto, a execução retorna MyCallingFunctione entra no loop for escrevendo 'Continuing to Execute' até 1000 vezes. Ao SaveChangesAsync()terminar, a execução retorna à SaveRecordsfunção, gravando Save Completeno console. Depois que tudo estiver SaveRecordsconcluído, a execução continuará da maneira MyCallingFunctioncorreta onde estava antes SaveChangesAsync(). Confuso? Aqui está um exemplo de saída:

Início da função
Salvar começando
Continuando a executar!
Continuando a executar!
Continuando a executar!
Continuando a executar!
Continuando a executar!
....
Continuando a executar!
Salvar concluído!
Continuando a executar!
Continuando a executar!
Continuando a executar!
....
Continuando a executar!
Função concluída!

Ou talvez:

Início da função
Salvar começando
Continuando a executar!
Continuando a executar!
Salvar concluído!
Continuando a executar!
Continuando a executar!
Continuando a executar!
....
Continuando a executar!
Função concluída!

Essa é a beleza de async await, seu código pode continuar a ser executado enquanto você espera que algo termine. Na realidade, você teria uma função mais parecida com esta como sua função de chamada:

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

Aqui, você tem quatro funções diferentes de salvar gravação em execução ao mesmo tempo . MyCallingFunctionserá concluído muito mais rápido usando do async awaitque se as SaveRecordsfunções individuais fossem chamadas em série.

A única coisa que ainda não mencionei é a awaitpalavra - chave. O que isso faz é parar a execução da função atual até que tudo Tasko que você está esperando seja concluído. Portanto, no caso do original MyCallingFunction, a linha Function Completenão será gravada no console até que a SaveRecordsfunção seja concluída.

Resumindo, se você tem uma opção de usar async await, você deve, pois isso aumentará muito o desempenho do seu aplicativo.


7
99% do tempo, ainda tenho que esperar que os valores sejam recebidos do banco de dados antes de continuar. Ainda devo usar o assíncrono? O assíncrono permite que 100 pessoas se conectem ao meu site de forma assíncrona? Se eu não usar async, isso significa que todos os 100 usuários terão que esperar na linha 1 de cada vez?
MIKE

6
Vale a pena notar: Gerar um novo thread do pool de threads torna o ASP um panda triste, pois você basicamente rouba um thread do ASP (o que significa que o thread não pode lidar com outras solicitações ou fazer nada, já que está preso em uma chamada de bloqueio). Se você usar await, no entanto, mesmo que VOCÊ não precise fazer mais nada após a chamada para SaveChanges, o ASP dirá "aha, este thread retornou aguardando uma operação assíncrona, isso significa que posso deixar este thread lidar com alguma outra solicitação nesse ínterim ! " Isso faz com que seu aplicativo seja dimensionado horizontalmente muito melhor.
Sara

3
Na verdade, eu fiz benchmarking de assíncrono para ser mais lento. E você já viu quantos threads estão disponíveis em um servidor ASP.Net típico? São dezenas de milhares. Portanto, a probabilidade de ficar sem threads para lidar com outras solicitações é muito improvável e, mesmo que você tivesse tráfego suficiente para saturar todas essas threads, seu servidor é realmente poderoso o suficiente para não se dobrar nesse caso? Afirmar que usar assíncrono em todos os lugares aumenta o desempenho é totalmente errado. Isso pode acontecer em certos cenários, mas na maioria das situações comuns será, de fato, mais lento. Compare e veja.
user3766657

@MIKE, enquanto um único usuário deve esperar que o banco de dados retorne os dados para continuar, seus outros usuários que usam seu aplicativo não o fazem. Embora o IIS crie um thread para cada solicitação (na verdade, é mais complexo do que isso), o thread em espera pode ser usado para lidar com outras solicitações, isso é importante para fins de escalabilidade. Imaginando cada solicitação, em vez de usar 1 thread em tempo integral, ele usa muitos threads mais curtos que podem ser reutilizados em outro lugar (também conhecido como outras solicitações).
Bart Calixto

1
Eu gostaria apenas de acrescentar que você deve sempre await para SaveChangesAsyncdesde EF não suporta múltiplos salva ao mesmo tempo. docs.microsoft.com/en-us/ef/core/saving/async Além disso, há uma grande vantagem em usar esses métodos assíncronos. Por exemplo, você pode continuar recebendo outras solicitações em seu webApi ao salvar dados ou fazer muitos sutuff, ou melhorar a experiência do usuário sem congelar a interface quando estiver em um aplicativo de desktop.
tgarcia

1

Minha explicação restante será baseada no seguinte trecho 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;
}

insira a descrição da imagem aqui

Observações: Como a parte síncrona (verde) de JobAsyncgira mais do que a tarefa t(vermelho), a tarefa tjá está concluída no ponto de await t. Como resultado, a continuação (azul) segue no mesmo fio que a verde. A parte síncrona de Main(branco) girará depois que a parte verde terminar de girar. É por isso que a parte síncrona no método assíncrono é 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;
}

insira a descrição da imagem aqui

Observações: Este caso é oposto ao primeiro caso. A parte síncrona (verde) dos JobAsyncspins mais curtos do que a tarefa t(vermelho), então a tarefa tnão foi concluída no ponto de await t. Como resultado, a continuação (azul) é executada na linha diferente da verde. A parte síncrona de Main(branco) ainda gira depois que a parte 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;
}

insira a descrição da imagem aqui

Observações: Este caso resolverá o problema dos casos anteriores sobre a parte síncrona no método assíncrono. A tarefa té aguardada imediatamente. Como resultado, a continuação (azul) é executada na linha diferente da verde. A parte síncrona de Main(branco) girará imediatamente em paralelo a JobAsync.

Se você quiser adicionar outros casos, fique à vontade para editar.


1

Esta afirmação está incorreta:

No lado do servidor, quando usamos métodos Async, também precisamos adicionar await.

Você não precisa adicionar "await", awaité apenas uma palavra-chave conveniente em C # que permite escrever mais linhas de código após a chamada, e essas outras linhas só serão executadas após a conclusão da operação Salvar. Mas, como você indicou, você pode fazer isso simplesmente ligando em SaveChangesvez de SaveChangesAsync.

Mas, fundamentalmente, uma chamada assíncrona envolve muito mais do que isso. A ideia aqui é que se houver outro trabalho que você possa fazer (no servidor) enquanto a operação Salvar estiver em andamento, você deve usar SaveChangesAsync. Não use "esperar". Basta ligar SaveChangesAsynce continuar a fazer outras coisas em paralelo. Isso inclui potencialmente, em um aplicativo da web, retornar uma resposta ao cliente antes mesmo que o salvamento seja concluído. Mas é claro, você ainda vai querer verificar o resultado final do Salvar para que, caso ele falhe, você possa comunicar isso ao seu usuário, ou registrá-lo de alguma forma.


4
Na verdade, você deseja aguardar essas chamadas, caso contrário, poderá executar consultas e / ou salvar dados simultaneamente usando a mesma instância DbContext e DbContext não é thread-safe. Além disso, o await facilita o tratamento de exceções. Sem esperar, você teria que armazenar a tarefa e verificar se está com defeito, mas sem saber quando a tarefa foi concluída, você não saberia quando verificar a menos que use '.ContinueWith', que requer muito mais reflexão do que esperar.
Pawel

22
Essa resposta é enganosa. Chamar um método assíncrono sem esperar torna-o um "disparar e esquecer". O método dispara e provavelmente será concluído em algum momento, mas você nunca saberá quando, e se ele lançar uma exceção, você nunca ouvirá sobre isso. Você não consegue sincronizar com sua conclusão. Esse tipo de comportamento potencialmente perigoso deve ser escolhido, não invocado com uma regra simples (e incorreta) como "espera no cliente, não espera no servidor".
John Melville

1
Este é um conhecimento muito útil que li na documentação, mas não havia realmente considerado. Portanto, você tem a opção de: 1. SaveChangesAsync () para "Disparar e esquecer", como diz John Melville ... o que é útil para mim em alguns casos. 2. aguarde SaveChangesAsync () para "Disparar, retornar ao chamador e, em seguida, executar algum código 'pós-salvamento' após a conclusão do salvamento. Peça muito útil. Obrigado.
Parrhesia Joe
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.