Обертывание синхронного кода в асинхронный вызов

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

Я вообще не рекомендую использовать Parallel 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 () в качестве первой строки вызываемого кода.

kernowcode
источник