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


86

Я шукав відмінності між двома парами вище, але не знайшов жодної статті, яка б чітко пояснювала це, а також коли використовувати ту чи іншу.

То яка різниця між SaveChanges()і SaveChangesAsync()?
А між Find()і FindAsync()?

На стороні сервера, коли ми використовуємо Asyncметоди, нам також потрібно додати await. Таким чином, я не думаю, що це асинхронно на стороні сервера.

Чи допомагає це лише запобігти блокуванню користувацького інтерфейсу в браузері на стороні клієнта? Або між ними є плюси і мінуси?


2
асинхронного набагато, набагато більше , ніж зупиняти клієнт UI нитку від блокування в клієнтських додатках. Я впевнений, що незабаром з’явиться відповідь експерта.
jdphenix

Відповіді:


174

Кожного разу, коли вам потрібно виконати дію на віддаленому сервері, ваша програма генерує запит, надсилає його, а потім чекає відповіді. Я буду використовувати 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і додати myListTo 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та вводить цикл написання циклу «Продовжуючи виконувати» до 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 людям підключатися до мого веб-сайту асинхронно? Якщо я не використовую асинхронізацію, це означає, що всі 100 користувачів повинні чекати в черзі 1 за раз?
MIKE

6
Варто зазначити: поява нового потоку з пулу потоків робить ASP сумною пандою, оскільки ви в основному грабуєте потік з ASP (це означає, що потік не може обробляти інші запити або взагалі нічого не робити, оскільки він застряг у блокувальному виклику). Якщо ви використовуєте await, навіть якщо ВАМ не потрібно робити нічого іншого після виклику SaveChanges, ASP скаже "ага, цей потік повернувся в очікуванні операції асинхронізації, це означає, що я можу дозволити цьому потоку обробляти якийсь інший запит тим часом ! " Це значно покращує масштаб програми по горизонталі.
сара

3
Насправді я визначив асинхронність повільнішою. І ви коли-небудь бачили, скільки потоків доступно на типовому сервері ASP.Net? Це як десятки тисяч. Тож шанси закінчити потоки для обробки подальших запитів дуже малоймовірні, і навіть якщо у вас було достатньо трафіку, щоб наситити всі ці потоки, ваш сервер насправді достатньо потужний, щоб у такому разі не згинатися? Стверджувати, що використання асинхронізації скрізь підвищує продуктивність, є абсолютно неправильним. Це може в певних сценаріях, але в найпоширеніших ситуаціях це насправді буде повільнішим. Орієнтир і дивись.
користувач3766657

@MIKE, хоча один користувач повинен чекати, поки база даних поверне дані, щоб продовжити, а інші користувачі, які використовують вашу програму, ні. Поки IIS створює потік для кожного запиту (насправді він складніший за цей), ваш потік, що очікує, можна використовувати для обробки інших запитів, це важливо для масштабованості afaik. Зображуючи кожен запит, замість того, щоб використовувати 1 потік у повному обсязі, він використовує безліч коротших потоків, які можна повторно використати де-небудь ще (тобто інші запити).
Барт Каліксто

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.

Вам не потрібно додавати "await", awaitце просто зручне ключове слово в C #, яке дозволяє вам писати більше рядків коду після дзвінка, а ці інші рядки виконуються лише після завершення операції збереження. Але як ви зазначили, ви можете досягти цього, просто зателефонувавши SaveChangesзамість цьогоSaveChangesAsync .

Але принципово, асинхронний виклик - це набагато більше, ніж це. Ідея тут полягає в тому, що якщо є інша робота, яку ви можете зробити (на сервері), поки триває операція збереження, тоді вам слід скористатися SaveChangesAsync. Не використовуйте "await". Просто зателефонуйте SaveChangesAsync, а потім продовжуйте займатися іншими справами паралельно. Сюди входить, можливо, у веб-програмі повернення відповіді клієнту ще до завершення збереження. Але, звичайно, ви все одно захочете перевірити кінцевий результат збереження, щоб у випадку його невдачі ви могли повідомити це своєму користувачеві або якось увійти до нього.


4
Ви насправді хочете чекати цих дзвінків, інакше ви можете запускати запити та одночасно зберігати дані, використовуючи один і той же екземпляр DbContext, і DbContext не є безпечним для потоку. На додачу до цього очікування полегшує обробку винятків. Не чекаючи, вам доведеться зберігати завдання та перевіряти, чи воно несправне, але, не знаючи, коли завдання виконане, ви не знали б, коли перевіряти, якщо ви не використовуєте ".ContinueWith", що вимагає набагато більшої продуманості, ніж очікування.
Павел

22
Ця відповідь оманлива. Виклик асинхронного методу без очікування робить його "вогнем і забудь". Метод вимикається і, можливо, колись завершиться, але ви ніколи не дізнаєтесь коли, і якщо він видасть виняток, ви ніколи про нього не почуєте, Ви не можете синхронізувати його завершення. Цей тип потенційно небезпечної поведінки слід вибирати, а не викликати його за допомогою простого (і некоректного) правила на кшталт "чекає на клієнті, не чекає на сервері".
Джон Мелвілл,

1
Це дуже корисне знання, яке я прочитав у документації, але насправді не розглядав. Отже, у вас є можливість: 1. ЗберегтиChangesAsync () на "Вогонь і забути", як каже Джон Мелвілл ... що мені корисно в деяких випадках. 2. дочекайтеся SaveChangesAsync () до "Запустити, поверніться до абонента, а потім виконайте якийсь код" після збереження "після завершення збереження. Дуже корисний фрагмент. Дякую.
Парресія Джо
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.