Ефективно використовувати async / wait на веб-API ASP.NET


114

Я намагаюся використовувати async/awaitфункцію ASP.NET у своєму проекті Web API. Я не дуже впевнений, чи вплине це на ефективність роботи мого сервісу Web API. Будь ласка, знайдіть нижче робочий процес та зразок коду з моєї заявки.

Робочий потік:

Застосування інтерфейсу користувача → Кінцева точка (контролер) веб-API → Спосіб виклику в рівні обслуговування Web API → Виклик іншої зовнішньої веб-служби. (Тут ми маємо взаємодії з БД тощо)

Контролер:

public async Task<IHttpActionResult> GetCountries()
{
    var allCountrys = await CountryDataService.ReturnAllCountries();

    if (allCountrys.Success)
    {
        return Ok(allCountrys.Domain);
    }

    return InternalServerError();
}

Службовий шар:

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountries()
{
    var response = _service.Process<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");

    return Task.FromResult(response);
}

Я перевірив вищевказаний код і працює. Але я не впевнений, чи це правильне використання async/await. Будь ласка, поділіться своїми думками.


Відповіді:


202

Я не дуже впевнений, чи вплине це на ефективність мого API.

Майте на увазі, що головна перевага асинхронного коду на стороні сервера - масштабованість . Це не призведе до того, що ваші запити запускаються швидше. У asyncсвоїй статті про asyncASP.NET я висвітлюю кілька міркувань "чи варто використовувати " .

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

Що стосується коду, це не асинхронно:

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountries()
{
  var response = _service.Process<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");
  return Task.FromResult(response);
}

Вам знадобиться справді асинхронна реалізація, щоб отримати переваги масштабування async:

public async Task<BackOfficeResponse<List<Country>>> ReturnAllCountriesAsync()
{
  return await _service.ProcessAsync<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");
}

Або (якщо ваша логіка у цьому методі насправді є лише переходом):

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountriesAsync()
{
  return _service.ProcessAsync<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");
}

Зауважте, що легше працювати з типу "всередину", ніж "зсередини". Іншими словами, не починайте з дії асинхронного контролера, а потім не примушуйте асинхронні методи низхідного потоку. Натомість визначте природні асинхронні операції (виклик зовнішніх API, запитів до бази даних тощо) та зробіть їх спочатку асинхронними на найнижчому рівні ( Service.ProcessAsync). Потім дозвольте asyncтрюку вгору, зробивши дії вашого контролера асинхронними як останній крок.

І ні в якому разі не слід використовувати Task.Runв цьому сценарії.


4
Дякую Стівену за ваші цінні коментарі. Я змінив всі мої методи службового рівня на асинхронність, і тепер я викликаю свій зовнішній дзвінок REST за допомогою методу ExecuteTaskAsync і працює як слід. Також дякую за повідомлення в блозі щодо асинхронізації та завдань. Це дійсно мені дуже допомогло зрозуміти початкове розуміння.
арп

5
Чи можете ви пояснити, чому ви не повинні використовувати Task.Run тут?
Маартен

1
@Maarten: Я пояснюю це (коротко) у статті, до якої я посилався . Я детальніше розглядаю свій блог .
Стівен Клірі

Я читав вашу статтю Стівен, і, здається, уникає щось згадувати. Коли надходить запит ASP.NET і починається, він працює на потоці пулу потоків, це добре. Але якщо він стає async, то так, він ініціює обробку async і цей потік повертається в пул відразу. Але робота, виконана в методі async, сама буде виконана на потоці пулу потоків!
Х'ю


12

Це правильно, але, можливо, не корисно.

Оскільки нічого чекати - немає дзвінків на блокування API, які могли б працювати асинхронно - тоді ви створюєте структури для відстеження асинхронної роботи (яка має накладні витрати), але потім не використовуєте цю можливість.

Наприклад, якщо сервісний рівень виконував операції з DB з Entity Framework, який підтримує асинхронні виклики:

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountries()
{
    using (db = myDBContext.Get()) {
      var list = await db.Countries.Where(condition).ToListAsync();

       return list;
    }
}

Ви дозволите робочій нитці зробити щось інше, поки db запитується (і таким чином зможете обробити інший запит).

Очікування - це те, що потрібно пройти весь шлях вниз: дуже важко вписатись у ретро-систему.


-1

Ви не використовуєте async / чекаєте ефективно, тому що нитка запиту буде заблокована під час виконання синхронного методуReturnAllCountries()

Потік, призначений для обробки запиту, буде простоюче чекати, поки ReturnAllCountries()це працює.

Якщо ви можете реалізувати ReturnAllCountries()асинхронність, ви побачите переваги масштабування. Це пов’язано з тим, що потік може бути відпущений назад до пулу потоків .NET для обробки іншого запиту під час ReturnAllCountries()виконання. Це дозволить вашій службі мати більш високу пропускну здатність, використовуючи нитки більш ефективно.


Це не правда. Розетка підключена, але потік, що обробляє запит, може робити щось інше, наприклад обробляти інший запит, очікуючи на дзвінок служби.
Гарр Годфрі

-8

Я б змінив ваш рівень обслуговування на:

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountries()
{
    return Task.Run(() =>
    {
        return _service.Process<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");
    }      
}

як це у вас є, ви все ще ведете свій _service.Processдзвінок синхронно і отримуєте дуже мало або не отримуєте користі від його очікування.

При такому підході ви перетворюєте потенційно повільний дзвінок у Task, запускаєте його та повертаєте його до очікування. Тепер ви отримаєте користь від очікування Task.


3
Згідно з посиланням на блог , я можу побачити, що навіть якщо ми використовуємо Task.Run метод все ще працює синхронно?
арп

Хм. Існує велика різниця між наведеним вище кодом та цим посиланням. Ваш Serviceне є асинхронним методом, і його не чекають у самому Serviceдзвінку. Я можу зрозуміти його думку, але я не вірю, що це стосується і тут.
Jonesopolis

8
Task.Runслід уникати на ASP.NET. Такий підхід дозволить зняти всі переваги з async/ awaitта фактично виконуватись гірше під навантаженням, ніж просто синхронний дзвінок.
Стівен Клірі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.