Асинхронізація Entity Framework, що підлягає запиту


97

Я працюю над деякими матеріалами веб-API, використовуючи Entity Framework 6, і одним із моїх методів контролера є "Отримати всіх", який очікує отримання вмісту таблиці з моєї бази даних як IQueryable<Entity>. У своєму сховищі мені цікаво, чи є якась вигідна причина робити це асинхронно, оскільки я новачок у використанні EF з асинхронізацією.

В основному це зводиться до

 public async Task<IQueryable<URL>> GetAllUrlsAsync()
 {
    var urls = await context.Urls.ToListAsync();
    return urls.AsQueryable();
 }

проти

 public IQueryable<URL> GetAllUrls()
 {
    return context.Urls.AsQueryable();
 }

Чи дійсно асинхронна версія принесе тут переваги продуктивності, чи я несу непотрібні накладні витрати, спершу проектуючи до Списку (використовуючи асинхронний розум), а ПОТІМ переходячи до IQueryable?


1
context.Urls має тип DbSet <URL>, який реалізує IQueryable <URL>, тому .AsQueryable () є зайвим. msdn.microsoft.com/en-us/library/gg696460(v=vs.113).aspx Припускаючи, що ви дотримувались шаблонів, які надає EF, або використовували інструмент, який створює для вас контекст.
Шон Б

Відповіді:


223

Здається, проблема полягає в тому, що ви неправильно зрозуміли, як асинхронізувати / очікувати роботу з Entity Framework.

Про структуру сутності

Отже, давайте розглянемо цей код:

public IQueryable<URL> GetAllUrls()
{
    return context.Urls.AsQueryable();
}

і приклад його використання:

repo.GetAllUrls().Where(u => <condition>).Take(10).ToList()

Що там відбувається?

  1. Ми отримуємо IQueryableоб’єкт (який ще не має доступу до бази даних)repo.GetAllUrls()
  2. За допомогою ми створюємо новий IQueryableоб’єкт із заданою умовою.Where(u => <condition>
  3. Ми створюємо новий IQueryableоб’єкт із зазначеним обмеженням підкачки.Take(10)
  4. Ми отримуємо результати з бази даних за допомогою .ToList(). Наш IQueryableоб'єкт компілюється в sql (як select top 10 * from Urls where <condition>). А база даних може використовувати індекси, сервер sql надсилає вам лише 10 об’єктів з вашої бази даних (не всі мільярди URL-адрес, що зберігаються в базі даних)

Добре, давайте розглянемо перший код:

public async Task<IQueryable<URL>> GetAllUrlsAsync()
{
    var urls = await context.Urls.ToListAsync();
    return urls.AsQueryable();
}

З тим самим прикладом використання ми отримали:

  1. Ми завантажуємо в пам’ять усі мільярди URL-адрес, що зберігаються у вашій базі даних await context.Urls.ToListAsync();.
  2. У нас переповнення пам’яті. Правильний спосіб убити ваш сервер

Про асинхронізацію / очікування

Чому переважно використовувати async / await? Давайте подивимось на цей код:

var stuff1 = repo.GetStuff1ForUser(userId);
var stuff2 = repo.GetStuff2ForUser(userId);
return View(new Model(stuff1, stuff2));

Що тут відбувається?

  1. Починаючи з рядка 1 var stuff1 = ...
  2. Ми надсилаємо запит на сервер sql, для якого ми хочемо отримати деякі речі1 userId
  3. Чекаємо (поточний потік заблоковано)
  4. Чекаємо (поточний потік заблоковано)
  5. .....
  6. Сервер SQL надішліть нам відповідь
  7. Переходимо до рядка 2 var stuff2 = ...
  8. Ми надсилаємо запит на сервер sql, для якого ми хочемо отримати деякі речі2 userId
  9. Чекаємо (поточний потік заблоковано)
  10. І знову
  11. .....
  12. Сервер SQL надішліть нам відповідь
  13. Ми робимо вигляд

Тож давайте розглянемо його асинхронну версію:

var stuff1Task = repo.GetStuff1ForUserAsync(userId);
var stuff2Task = repo.GetStuff2ForUserAsync(userId);
await Task.WhenAll(stuff1Task, stuff2Task);
return View(new Model(stuff1Task.Result, stuff2Task.Result));

Що тут відбувається?

  1. Ми надсилаємо запит на сервер sql, щоб отримати stuff1 (рядок 1)
  2. Ми надсилаємо запит на сервер sql, щоб отримати stuff2 (рядок 2)
  3. Ми чекаємо відповідей від сервера sql, але поточний потік не заблокований, він може обробляти запити інших користувачів
  4. Ми робимо вигляд

Правильний спосіб це зробити

Так хороший код тут:

using System.Data.Entity;

public IQueryable<URL> GetAllUrls()
{
   return context.Urls.AsQueryable();
}

public async Task<List<URL>> GetAllUrlsByUser(int userId) {
   return await GetAllUrls().Where(u => u.User.Id == userId).ToListAsync();
}

Зверніть увагу, що ви повинні додати using System.Data.Entity, щоб використовувати метод ToListAsync()для IQueryable.

Зауважте, що якщо вам не потрібні фільтрація, підкачки та інше, вам не потрібно працювати IQueryable. Ви можете просто використовувати await context.Urls.ToListAsync()і працювати з матеріалізованим List<Url>.


3
@Korijn, дивлячись на зображення i2.iis.net/media/7188126/… із « Введення в архітектуру IIS», я можу сказати, що всі запити в IIS обробляються асинхронно
Віктор Лова,

7
Оскільки ви не впливаєте на результат, встановлений у GetAllUrlsByUserметоді, вам не потрібно робити його асинхронним. Просто поверніть Завдання і збережіть собі непотрібний автомат стану від генерування компілятором.
Джонатан Суллінгер

1
@JohnathonSullinger Хоча це могло б працювати в щасливому потоці, хіба це не має побічного ефекту від того, що будь-який виняток не з’явиться тут і не пошириться на перше місце, яке очікує? (Не те, що це обов’язково погано, але це зміна поведінки?)
Генрі Був

9
Цікаво, що ніхто не помічає, що другий приклад коду в "Про асинхронізацію / очікування" є абсолютно безглуздим, оскільки він видасть виняток, оскільки ні EF, ні EF Core не є безпечними для потоків, тому спроба паралельного запуску просто викине виняток
Tseng

1
Хоча ця відповідь правильна, я рекомендую уникати використання, asyncі awaitякщо ви НЕ робите нічого зі списком. Дозвольте абоненту await. Очікуючи дзвінка на цьому етапі, return await GetAllUrls().Where(u => u.User.Id == userId).ToListAsync();ви створюєте додаткову асинхронну обгортку під час декомпіляції збірки та перегляду IL.
Алі Хакпурі

10

Існує величезна різниця у прикладі, який ви опублікували, перша версія:

var urls = await context.Urls.ToListAsync();

Це погано , в основному воно робить select * from table, повертає всі результати в пам’ять, а потім застосовує whereпроти цього в колекції пам’яті, а не робить select * from table where...проти бази даних.

Другий метод фактично не потрапить у базу даних, поки запит не буде застосовано до IQueryable(можливо, за допомогою операції .Where().Select()стилю linq, яка поверне лише значення db, які відповідають запиту.

Якщо ваші приклади були порівнянними, asyncверсія зазвичай буде дещо повільнішою на запит, оскільки в машині стану більше накладних витрат, які компілятор генерує, щоб дозволити asyncфункціональність.

Однак основна відмінність (і перевага) полягає в тому, що asyncверсія дозволяє отримувати одночасні запити, оскільки вона не блокує обробний потік, поки вона чекає завершення введення-виводу (запит db, доступ до файлів, веб-запит тощо).


7
доки запит не буде застосовано до IQueryable .... ані IQueryable.Where та IQueryable.Select примусово виконати запит. Попередній застосовує предикат, а останній застосовує проекцію. Він не виконується доти, доки не буде використаний матеріалізуючий оператор, такий як ToList, ToArray, Single або First.
JJS

0

Коротко кажучи,
IQueryableпризначений для відкладання процесу RUN і спочатку побудови виразу спільно з іншими IQueryableвиразами, а потім інтерпретації та запуску виразу в цілому.
Але ToList()метод (або декілька подібних методів) передбачає миттєвий запуск виразу "як є".
Ваш перший метод ( GetAllUrlsAsync) буде запущений негайно, оскільки за ним IQueryableслідує ToListAsync()метод. отже, він запускається миттєво (асинхронно) і повертає купу IEnumerables.
Тим часом ваш другий метод ( GetAllUrls) не буде запущений. Натомість він повертає вираз, і CALLER цього методу відповідає за запуск виразу.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.