Envolvendo o código síncrono em uma chamada assíncrona

95

Eu tenho um método no aplicativo ASP.NET, que consome muito tempo para ser concluído. Uma chamada para esse método pode ocorrer até 3 vezes durante uma solicitação do usuário, dependendo do estado do cache e dos parâmetros fornecidos pelo usuário. Cada chamada leva cerca de 1 a 2 segundos para ser concluída. O método em si é uma chamada síncrona para o serviço e não há possibilidade de substituir a implementação.
Portanto, a chamada síncrona para o serviço é semelhante a esta:

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

E o uso do método é (observe que a chamada do método pode acontecer mais de uma vez):

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

Tentei mudar a implementação do meu lado para fornecer um trabalho simultâneo desse método, e aqui está o que eu vim até agora.

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

Uso (parte do código "fazer outras coisas" é executado simultaneamente com a chamada para o serviço):

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

Minha pergunta é a seguinte. Devo usar a abordagem certa para acelerar a execução no aplicativo ASP.NET ou estou fazendo um trabalho desnecessário tentando executar o código síncrono de forma assíncrona? Alguém pode explicar por que a segunda abordagem não é uma opção no ASP.NET (se realmente não for)? Além disso, se essa abordagem for aplicável, preciso chamar esse método de forma assíncrona se for a única chamada que podemos realizar no momento (eu tenho esse caso, quando não há outra coisa a fazer enquanto aguardamos a conclusão)?
A maioria dos artigos na rede sobre esse assunto aborda o uso de async-awaitabordagem com o código, que já fornece awaitablemétodos, mas não é o meu caso. Aquié o bom artigo que descreve meu caso, que não descreve a situação de chamadas paralelas, recusando a opção de encerrar a chamada de sincronização, mas na minha opinião minha situação é exatamente a ocasião para fazê-lo.
Agradecemos antecipadamente por ajuda e dicas.

Eadel
fonte

Respostas:

116

É importante fazer uma distinção entre dois tipos diferentes de simultaneidade. Simultaneidade assíncrona é quando você tem várias operações assíncronas em andamento (e como cada operação é assíncrona, nenhuma delas está realmente usando um thread ). Simultaneidade paralela é quando você tem vários threads fazendo uma operação separada.

A primeira coisa a fazer é reavaliar essa suposição:

O método em si é uma chamada síncrona para o serviço e não há possibilidade de substituir a implementação.

Se o seu "serviço" for um serviço da Web ou qualquer outro vinculado a E / S, a melhor solução é escrever uma API assíncrona para ele.

Prosseguirei com a suposição de que seu "serviço" é uma operação vinculada à CPU que deve ser executada na mesma máquina do servidor web.

Se for esse o caso, a próxima coisa a avaliar é outra suposição:

Preciso que a solicitação seja executada mais rapidamente.

Você tem certeza absoluta de que é isso que você precisa fazer? Em vez disso, há alguma alteração de front-end que você possa fazer - por exemplo, iniciar a solicitação e permitir que o usuário faça outro trabalho durante o processamento?

Prosseguirei com a suposição de que sim, você realmente precisa fazer com que a solicitação individual seja executada mais rapidamente.

Nesse caso, você precisará executar o código paralelo em seu servidor web. Definitivamente, isso não é recomendado em geral porque o código paralelo usará threads que o ASP.NET pode precisar para lidar com outras solicitações e, ao remover / adicionar threads, irá eliminar a heurística do pool de threads do ASP.NET. Portanto, essa decisão tem impacto em todo o servidor.

Ao usar código paralelo no ASP.NET, você está tomando a decisão de realmente limitar a escalabilidade do seu aplicativo da web. Você também pode ver uma quantidade razoável de rotatividade de threads, especialmente se suas solicitações tiverem rajadas. Recomendo apenas usar código paralelo no ASP.NET se você souber que o número de usuários simultâneos será muito baixo (ou seja, não é um servidor público).

Portanto, se você chegou até aqui e tem certeza de que deseja fazer processamento paralelo no ASP.NET, você tem algumas opções.

Um dos métodos mais fáceis é usar Task.Run, muito semelhante ao seu código existente. No entanto, não recomendo implementar um CalculateAsyncmétodo, pois isso implica que o processamento é assíncrono (o que não é). Em vez disso, use Task.Runno ponto da chamada:

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

Alternativamente, se ele funciona bem com o seu código, você pode usar o Paralleltipo, ou seja, Parallel.For, Parallel.ForEach, ou Parallel.Invoke. A vantagem do Parallelcódigo é que o thread de solicitação é usado como um dos threads paralelos e, em seguida, retoma a execução no contexto do thread (há menos troca de contexto do que no asyncexemplo):

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

Eu não recomendo usar o Parallel LINQ (PLINQ) no ASP.NET.

Stephen Cleary
fonte
1
Muito obrigado por uma resposta detalhada. Mais uma coisa que quero esclarecer. Quando eu começo a execução usando Task.Run, o conteúdo é executado em outro thread, que é obtido do pool de threads. Então, não há necessidade de envolver chamadas de bloqueio (como a minha chamada para serviço) em Runs, porque elas sempre consumirão um thread cada, que será bloqueado durante a execução do método. Em tal situação, o único benefício que resta async-awaitno meu caso é realizar várias ações ao mesmo tempo. Por favor me corrija se eu estiver errado.
Eadel
2
Sim, o único benefício que você está obtendo de Run(ou Parallel) é a simultaneidade. As operações ainda estão bloqueando um thread. Como você diz que o serviço é um serviço da Web, não recomendo usar Runou Parallel; em vez disso, escreva uma API assíncrona para o serviço.
Stephen Cleary
3
@StephenCleary. o que você quer dizer com "... e como cada operação é assíncrona, nenhuma delas está usando um thread ..." obrigado!
alltej
3
@alltej: Para código verdadeiramente assíncrono, não há thread .
Stephen Cleary
4
@JoshuaFrank: Não diretamente. O código por trás de uma API síncrona deve bloquear o thread de chamada, por definição. Você pode usar uma API assíncrona como BeginGetResponsee iniciar várias delas e, em seguida, bloquear (de forma síncrona) até que todas estejam concluídas, mas esse tipo de abordagem é complicado - consulte blog.stephencleary.com/2012/07/dont-block-on -async-code.html e msdn.microsoft.com/en-us/magazine/mt238404.aspx . Normalmente, é mais fácil e limpo adotar asynctotalmente, se possível.
Stephen Cleary de
1

Eu descobri que o código a seguir pode converter uma tarefa para sempre ser executada de forma assíncrona

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

e eu usei da seguinte maneira

await ForceAsync(() => AsyncTaskWithNoAwaits())

Isso executará qualquer tarefa de forma assíncrona para que você possa combiná-los em cenários WhenAll, WhenAny e outros usos.

Você também pode simplesmente adicionar Task.Yield () como a primeira linha do código chamado.

kernowcode
fonte