Обернення синхронного коду в асинхронний виклик


97

У мене є метод у програмі ASP.NET, який займає досить багато часу для його завершення. Виклик цього методу може відбуватися до 3 разів під час одного запиту користувача, залежно від стану кешу та параметрів, які надає користувач. Кожен дзвінок займає близько 1-2 секунд. Сам метод - це синхронний виклик служби, і немає можливості замінити реалізацію.
Отже, синхронний дзвінок до служби виглядає приблизно так:

public OutputModel Calculate(InputModel input)
{
    // do some stuff
    return Service.LongRunningCall(input);
}

І використання методу є (зауважте, що виклик методу може траплятися більше одного разу):

private void MakeRequest()
{
    // a lot of other stuff: preparing requests, sending/processing other requests, etc.
    var myOutput = Calculate(myInput);
    // stuff again
}

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

public async Task<OutputModel> CalculateAsync(InputModel input)
{
    return await Task.Run(() =>
    {
        return Calculate(input);
    });
}

Використання (частина коду "робити інші речі" виконується одночасно із викликом до служби):

private async Task MakeRequest()
{
    // do some stuff
    var task = CalculateAsync(myInput);
    // do other stuff
    var myOutput = await task;
    // some more stuff
}

Моє запитання таке. Чи використовую я правильний підхід для прискорення виконання в програмі ASP.NET чи я виконую непотрібну роботу, намагаючись запустити синхронний код асинхронно? Хто-небудь може пояснити, чому другий підхід не є варіантом в ASP.NET (якщо він насправді не є)? Крім того, якщо такий підхід застосовний, чи потрібно мені викликати такий метод асинхронно, якщо це єдиний виклик, який ми можемо виконати на даний момент (у мене такий випадок, коли жодних інших речей не потрібно робити, поки чекаємо завершення)?
Більшість статей у мережі на цю тему охоплює використання async-awaitпідходу з кодом, який вже надає awaitableметоди, але це не мій випадок. Осьце приємна стаття, що описує мій випадок, яка не описує ситуацію паралельних викликів, відмовляючись від можливості обгортання виклику синхронізації, але, на мій погляд, моя ситуація - саме привід це зробити.
Заздалегідь дякую за допомогу та поради.

Відповіді:


119

Важливо провести різницю між двома різними типами паралельності. Асинхронна паралельність - це коли у вас є кілька асинхронних операцій у польоті (і оскільки кожна операція є асинхронною, жодна з них насправді не використовує потік ). Паралельна паралельність - це коли у вас є кілька потоків, кожна з яких виконує окрему операцію.

Перше, що потрібно зробити, це переоцінити це припущення:

Сам метод - це синхронний виклик служби, і немає можливості замінити реалізацію.

Якщо ваша "послуга" - це веб- служба або щось інше, що пов'язане з введенням / виводом, то найкращим рішенням є написання асинхронного API для неї.

Я продовжуватиму з припущенням, що ваша "послуга" - це операція, пов'язана з процесором, яка повинна виконуватися на тій самій машині, що і веб-сервер.

Якщо це так, то наступне, що потрібно оцінити, - це інше припущення:

Мені потрібен запит для швидшого виконання.

Ви абсолютно впевнені, що це те, що вам потрібно зробити? Чи можна замість цього внести будь-які інтерфейсні зміни - наприклад, розпочати запит і дозволити користувачеві виконувати інші роботи під час його обробки?

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

У цьому випадку вам потрібно буде виконати паралельний код на своєму веб-сервері. Це, безумовно, не рекомендується взагалі, оскільки паралельний код буде використовувати потоки, які ASP.NET, можливо, знадобляться для обробки інших запитів, і, видаливши / додавши потоки, це відкине евристику пулу потоків ASP.NET. Отже, це рішення впливає на весь ваш сервер.

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

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

Одним із найпростіших методів є використання Task.Run, дуже схоже на ваш існуючий код. Однак я не рекомендую застосовувати CalculateAsyncметод, оскільки це означає, що обробка є асинхронною (а це не так). Натомість використовуйте Task.Runв точці дзвінка:

private async Task MakeRequest()
{
  // do some stuff
  var task = Task.Run(() => Calculate(myInput));
  // do other stuff
  var myOutput = await task;
  // some more stuff
}

З іншого боку , якщо він добре працює з вашим кодом, ви можете використовувати Parallelтип, тобто Parallel.For, Parallel.ForEach, або Parallel.Invoke. Перевага Parallelкоду полягає в тому, що потік запиту використовується як один із паралельних потоків, а потім відновлює виконання в контексті потоку (перемикання контексту менше, ніж у asyncприкладі):

private void MakeRequest()
{
  Parallel.Invoke(() => Calculate(myInput1),
      () => Calculate(myInput2),
      () => Calculate(myInput3));
}

Я взагалі не рекомендую використовувати паралельний LINQ (PLINQ) на ASP.NET.


1
Велике спасибі за детальну відповідь. Ще одне, що я хочу пояснити. Коли я починаю виконувати з використанням Task.Run, вміст запускається в іншому потоці, який береться з пулу потоків. Тоді взагалі немає необхідності обертати блокуючі виклики (наприклад, мій виклик до служби) в Runs, оскільки вони завжди будуть споживати по одному потоку, який буде заблокований під час виконання методу. У такій ситуації єдиною перевагою, яка залишилася async-awaitв моєму випадку, є виконання одночасно декількох дій. Будь ласка, виправте мене, якщо я помиляюся.
Eadel

2
Так, єдиною перевагою, яку ви отримуєте від Run(або Parallel), є паралельність. Кожна операція все ще блокує нитку. Оскільки ви говорите, що послуга є веб-службою, то я не рекомендую використовувати Runабо Parallel; натомість напишіть асинхронний API для служби.
Стівен Клірі

3
@StephenCleary. що ви маєте на увазі під "... і оскільки кожна операція є асинхронною, жодна з них насправді не використовує нитку ..." дякую!
alltej

3
@alltej: Для справді асинхронного коду немає потоку .
Стівен Клірі

4
@JoshuaFrank: Не безпосередньо. Код, що стоїть за синхронним API, за визначенням повинен блокувати виклик потоку. Ви можете використовувати асинхронний API, як BeginGetResponseі запустити декілька з них, а потім (синхронно) блокувати, поки всі вони не будуть завершені, але такий підхід хитрий - див. Blog.stephencleary.com/2012/07/dont-block-on -async-code.html та msdn.microsoft.com/en-us/magazine/mt238404.aspx . Як правило, легше та чистіше прийняти його asyncдо кінця, якщо це можливо.
Стівен Клірі

1

Я виявив, що наступний код може перетворити Завдання, щоб воно завжди працювало асинхронно

private static async Task<T> ForceAsync<T>(Func<Task<T>> func)
{
    await Task.Yield();
    return await func();
}

і я використав його наступним чином

await ForceAsync(() => AsyncTaskWithNoAwaits())

Це виконає будь-яке Завдання асинхронно, щоб Ви могли комбінувати їх у сценаріях WhenAll, WhenAny та інших цілях.

Ви також можете просто додати Task.Yield () як перший рядок вашого викликаного коду.

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