Entity Framework SaveChanges () против SaveChangesAsync () и Find () против FindAsync ()

87

Я искал различия между двумя парами, указанными выше, но не нашел статей, четко объясняющих об этом, а также о том, когда использовать ту или другую.

Так в чем разница между SaveChanges()и SaveChangesAsync()?
А между Find()и FindAsync()?

На стороне сервера, когда мы используем Asyncметоды, нам также нужно добавить await. Таким образом, я не думаю, что это асинхронно на стороне сервера.

Помогает ли это только предотвратить блокировку пользовательского интерфейса в браузере на стороне клиента? Или между ними есть какие-то плюсы и минусы?

Hien Tran
источник
2
async - это намного больше, чем просто предотвращение блокировки потока пользовательского интерфейса клиента в клиентских приложениях. Я уверен, что скоро придет экспертный ответ.
jdphenix

Ответы:

175

Каждый раз, когда вам нужно выполнить действие на удаленном сервере, ваша программа генерирует запрос, отправляет его, а затем ожидает ответа. Я буду использовать SaveChanges()и SaveChangesAsync()в качестве примера, но то же самое применимо к Find()и FindAsync().

Допустим, у вас есть список myListиз более чем 100 элементов, которые нужно добавить в свою базу данных. Чтобы вставить это, ваша функция будет выглядеть примерно так:

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

Сначала вы создаете экземпляр MyEDM, добавляете список myListв таблицу MyTable, а затем вызываете, SaveChanges()чтобы сохранить изменения в базе данных. Он работает так, как вы хотите, записи фиксируются, но ваша программа не может делать ничего другого, пока фиксация не завершится. Это может занять много времени в зависимости от того, что вы делаете. Если вы фиксируете изменения в записях, сущность должна фиксировать их по одному (у меня однажды было сохранение, занимающее 2 минуты для обновлений)!

Чтобы решить эту проблему, вы можете сделать одно из двух. Во-первых, вы можете запустить новый поток для обработки вставки. Хотя это освободит вызывающий поток для продолжения выполнения, вы создали новый поток, который просто будет сидеть и ждать. В этих накладных расходах нет необходимости, и именно это async awaitрешает шаблон.

Для операций ввода-вывода awaitбыстро становится вашим лучшим другом. Взяв фрагмент кода сверху, мы можем изменить его так:

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

Это очень небольшое изменение, но оно оказывает глубокое влияние на эффективность и производительность вашего кода. Так что же происходит? Начало кода такое же, вы создаете экземпляр MyEDMи добавляете свой myListв MyTable. Но когда вы вызываете await context.SaveChangesAsync(), выполнение кода возвращается к вызывающей функции! Итак, пока вы ждете фиксации всех этих записей, ваш код может продолжать выполнение. Скажем, функция, содержащая приведенный выше код, имела подпись public async Task SaveRecords(List<MyTable> saveList), вызывающая функция могла бы выглядеть так:

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

Зачем нужна такая функция, я не знаю, но то, что она выводит, показывает, как она async awaitработает. Сначала давайте разберемся, что происходит.

Начинается выполнение MyCallingFunction, Function Startingзатем Save Startingзаписывается в консоль, после чего SaveChangesAsync()вызывается функция . На этом этапе выполнение возвращается к MyCallingFunctionциклу for и переходит к нему, записывая «Continuing to Execute» до 1000 раз. По SaveChangesAsync()завершении выполнение возвращается к SaveRecordsфункции, записывая Save Completeее в консоль. Как только все будет SaveRecordsзавершено, выполнение продолжится так MyCallingFunction, как было до SaveChangesAsync()завершения. Смущенный? Вот пример вывода:

Запуск функции
Сохранить начало
Продолжаем выполнять!
Продолжаем выполнять!
Продолжаем выполнять!
Продолжаем выполнять!
Продолжаем выполнять!
....
Продолжаем выполнять!
Сохранение завершено!
Продолжаем выполнять!
Продолжаем выполнять!
Продолжаем выполнять!
....
Продолжаем выполнять!
Функция завершена!

Или, может быть:

Запуск функции
Сохранить начало
Продолжаем выполнять!
Продолжаем выполнять!
Сохранение завершено!
Продолжаем выполнять!
Продолжаем выполнять!
Продолжаем выполнять!
....
Продолжаем выполнять!
Функция завершена!

В этом прелесть того async await, что ваш код может продолжать работать, пока вы ждете, пока что-то завершится. На самом деле у вас будет функция, более похожая на эту, как ваша вызывающая функция:

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

Здесь у вас есть четыре различных сохранить запись функций , идущих одновременно . MyCallingFunctionбудет выполняться намного быстрее, async awaitчем если бы отдельные SaveRecordsфункции вызывались последовательно.

Единственное, что я еще не затронул, - это awaitключевое слово. Это останавливает выполнение текущей функции до тех пор, пока все, что Taskвы ожидаете, не завершится. Таким образом, в случае оригинала MyCallingFunctionстрока Function Completeне будет записана в консоль до завершения SaveRecordsфункции.

Короче говоря, если у вас есть возможность использовать async await, вы должны, поскольку это значительно повысит производительность вашего приложения.

Джейкоб Ламберт
источник
7
В 99% случаев мне все еще приходится ждать получения значений из базы данных, прежде чем я смогу продолжить. Следует ли мне по-прежнему использовать асинхронный режим? Позволяет ли async 100 людям подключаться к моему сайту асинхронно? Если я не использую async, означает ли это, что все 100 пользователей должны ждать очереди 1 за раз?
МАЙК
6
Стоит отметить: создание нового потока из пула потоков превращает ASP в унылую панду, поскольку вы в основном грабите поток из ASP (что означает, что поток не может обрабатывать другие запросы или вообще что-либо делать, поскольку он застрял в блокирующем вызове). awaitОднако, если вы используете , даже если ВАМ не нужно больше ничего делать после вызова SaveChanges, ASP скажет: «Ага, этот поток вернулся в ожидании асинхронной операции, это означает, что я могу позволить этому потоку обрабатывать другой запрос тем временем. ! " Это значительно улучшает масштабирование вашего приложения по горизонтали.
Сара
3
На самом деле я тестировал async, чтобы он работал медленнее. А вы когда-нибудь видели, сколько потоков доступно на типичном сервере ASP.Net? Это вроде десятков тысяч. Таким образом, вероятность того, что потоки закончатся для обработки дальнейших запросов, очень маловероятна, и даже если у вас было достаточно трафика, чтобы заполнить все эти потоки, действительно ли ваш сервер достаточно мощный, чтобы в любом случае не прогибаться? Совершенно неверно утверждать, что использование async повсюду увеличивает производительность. Это может быть в определенных сценариях, но в большинстве случаев на самом деле это будет медленнее. Оцените и посмотрите.
user3766657
@MIKE, в то время как один пользователь должен ждать, пока база данных вернет данные, чтобы продолжить, другие пользователи, использующие ваше приложение, этого не делают. Хотя IIS создает поток для каждого запроса (на самом деле он более сложный), ваш ожидающий поток может использоваться для обработки других запросов, это важно для масштабируемости afaik. Визуализация каждого запроса вместо использования одного потока на постоянной основе использует множество более коротких потоков, которые можно повторно использовать в другом месте (например, в других запросах).
Барт Каликсто
1
Я хотел бы только добавить , что вы всегда должны await для SaveChangesAsyncпоскольку EF не поддерживает несколько экономит одновременно. docs.microsoft.com/en-us/ef/core/saving/async Кроме того, использование этих асинхронных методов дает большое преимущество. Например, вы можете продолжать получать другие запросы в своем webApi при сохранении данных или выполнении большого количества операций, или улучшить пользовательский интерфейс, не замораживая интерфейс, когда вы находитесь в настольном приложении.
tgarcia
1

Мое оставшееся объяснение будет основано на следующем фрагменте кода.

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

Случай 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;
}

введите описание изображения здесь

Примечания: Поскольку синхронная часть (зеленый) JobAsyncвращается дольше, чем задача t(красный), то задача tуже завершена на момент await t. В результате продолжение (синее) выполняется в том же потоке, что и зеленое. Синхронная часть Main(белая) будет вращаться после того, как зеленая закончит вращение. Поэтому синхронная часть в асинхронном методе проблематична.

Случай 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;
}

введите описание изображения здесь

Примечания: Этот случай противоположен первому. Синхронная часть (зеленый цвет) JobAsyncвращается короче, чем задача t(красный цвет), тогда задача tне была завершена на момент await t. В результате продолжение (синее) выполняется в другом потоке, что и зеленый. Синхронная часть Main(белая) продолжает вращаться после того, как зеленая закончила вращение.

Случай 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;
}

введите описание изображения здесь

Замечания: Этот случай решит проблему в предыдущих случаях о синхронной части в асинхронном методе. Задача tсразу же ожидается. В результате продолжение (синее) выполняется в другом потоке, что и зеленый. Синхронная часть Main(белая) будет вращаться сразу параллельно JobAsync.

Если вы хотите добавить другие случаи, не стесняйтесь редактировать.

Программист, ориентированный на деньги
источник
1

Это утверждение неверно:

На стороне сервера, когда мы используем методы Async, нам также необходимо добавить ожидание.

Вам не нужно добавлять «ожидание», awaitэто просто удобное ключевое слово в C #, которое позволяет вам писать больше строк кода после вызова, и эти другие строки будут выполняться только после завершения операции сохранения. Но, как вы отметили, этого можно добиться, просто позвонив SaveChangesвместо SaveChangesAsync.

Но по сути, асинхронный вызов - это гораздо больше. Идея здесь в том, что если есть другая работа, которую вы можете выполнять (на сервере) во время операции сохранения, то вам следует использовать SaveChangesAsync. Не используйте "ожидание". Просто позвоните SaveChangesAsync, а потом продолжайте параллельно заниматься другими делами. Это включает в себя потенциально в веб-приложении возврат ответа клиенту даже до завершения сохранения. Но, конечно, вы все равно захотите проверить окончательный результат сохранения, чтобы в случае сбоя вы могли сообщить об этом своему пользователю или как-то зарегистрировать его.

Раджив Гоэль
источник
4
На самом деле вы хотите дождаться этих вызовов, иначе вы можете запускать запросы и / или сохранять данные одновременно, используя один и тот же экземпляр DbContext, а DbContext не является потокобезопасным. Вдобавок к этому await упрощает обработку исключений. Без await вам пришлось бы сохранить задачу и проверить, не повреждена ли она, но, не зная, когда задача завершена, вы бы не знали, когда проверять, если не используете '.ContinueWith', который требует гораздо больше размышлений, чем await.
Павел
22
Этот ответ обманчив. Вызов асинхронного метода без ожидания делает его «выстрелил и забыл». Метод отключается и, вероятно, когда-нибудь завершится, но вы никогда не узнаете, когда, и если он вызовет исключение, вы никогда не услышите об этом, вы не сможете синхронизироваться с его завершением. Этот вид потенциально опасного поведения следует выбирать, а не вызывать с помощью простого (и неправильного) правила вроде «ожидает на клиенте, не ожидает на сервере».
Джон Мелвилл
1
Это очень полезная информация, которую я прочитал в документации, но на самом деле не рассматривал. Итак, у вас есть возможность: 1. SaveChangesAsync () до «Запустить и забыть», как говорит Джон Мелвилл ... что в некоторых случаях полезно для меня. 2. await SaveChangesAsync () to «Сработает, вернется к вызывающему, а затем выполнит некоторый код« после сохранения »после того, как сохранение будет завершено. Очень полезный фрагмент. Спасибо.
Парресия Джо