Como evitar violar o princípio DRY quando você precisa ter versões de código assíncronas e sincronizadas?

15

Estou trabalhando em um projeto que precisa oferecer suporte à versão assíncrona e sincronizada de uma mesma lógica / método. Então, por exemplo, eu preciso ter:

public class Foo
{
   public bool IsIt()
   {
      using (var conn = new SqlConnection(DB.ConnString))
      {
         return conn.Query<bool>("SELECT IsIt FROM SomeTable");
      }
   }

   public async Task<bool> IsItAsync()
   {
      using (var conn = new SqlConnection(DB.ConnString))
      {
         return await conn.QueryAsync<bool>("SELECT IsIt FROM SomeTable");
      }
   }
}

A lógica assíncrona e sincronizada para esses métodos é idêntica em todos os aspectos, exceto que uma é assíncrona e a outra não. Existe uma maneira legítima de evitar violar o princípio DRY nesse tipo de cenário? Vi que as pessoas dizem que você pode usar GetAwaiter (). GetResult () em um método assíncrono e invocá-lo a partir do seu método de sincronização? Esse segmento é seguro em todos os cenários? Existe outra maneira melhor de fazer isso ou sou forçado a duplicar a lógica?

Marko
fonte
11
Você pode falar um pouco mais sobre o que está fazendo aqui? Especificamente, como você está conseguindo a assincronia no seu método assíncrono? A CPU de trabalho de alta latência está vinculada ou IO?
Eric Lippert
"um é assíncrono e outro não" é a diferença no código do método real ou apenas na interface? (Claramente, apenas para a interface que você pode return Task.FromResult(IsIt());)
Alexei Levenkov
Além disso, presumivelmente, as versões síncrona e assíncrona são de alta latência. Em que circunstâncias o chamador usará a versão síncrona e como esse chamador mitigará o fato de que o chamado pode levar bilhões de nanossegundos? O chamador da versão síncrona não se importa com a suspensão da interface do usuário? Não existe interface do usuário? Conte-nos mais sobre o cenário.
Eric Lippert
@EricLippert Adicionei um exemplo fictício específico apenas para dar uma idéia de como as bases de dois códigos são idênticas, exceto pelo fato de que uma é assíncrona. Você pode imaginar facilmente um cenário em que esse método é muito mais complexo e as linhas e linhas de código precisam ser duplicadas.
Marko
@AlexeiLevenkov A diferença está realmente no código, eu adicionei algum código fictício para demonstrá-lo.
Marko

Respostas:

15

Você fez várias perguntas na sua pergunta. Vou dividi-los um pouco diferente do que você fez. Mas primeiro deixe-me responder diretamente à pergunta.

Todos nós queremos uma câmera leve, de alta qualidade e barata, mas, como diz o ditado, você pode obter no máximo duas dessas três. Você está na mesma situação aqui. Você deseja uma solução eficiente, segura e que compartilhe código entre os caminhos síncrono e assíncrono. Você só vai conseguir dois desses.

Deixe-me explicar por que isso é. Vamos começar com esta pergunta:


Vi que as pessoas dizem que você poderia usar GetAwaiter().GetResult() um método assíncrono e invocá-lo a partir do seu método de sincronização? Esse segmento é seguro em todos os cenários?

O objetivo desta pergunta é "posso compartilhar os caminhos síncronos e assíncronos fazendo com que o caminho síncrono simplesmente faça uma espera síncrona na versão assíncrona?"

Deixe-me ser super claro neste ponto, porque é importante:

VOCÊ DEVE IMEDIATAMENTE PARAR DE TER QUALQUER CONSELHO DESSAS PESSOAS .

Esse é um conselho extremamente ruim. É muito perigoso buscar resultados de uma tarefa assíncrona de forma síncrona, a menos que você tenha evidências de que a tarefa foi concluída normalmente ou de forma anormal .

A razão pela qual esse conselho é extremamente ruim é, bem, considere esse cenário. Você deseja cortar a grama, mas a lâmina do cortador de grama está quebrada. Você decide seguir este fluxo de trabalho:

  • Peça um novo blade em um site. Esta é uma operação assíncrona de alta latência.
  • Espere de forma síncrona - ou seja, durma até ter a lâmina na mão .
  • Verifique periodicamente a caixa de correio para ver se o blade chegou.
  • Retire a lâmina da caixa. Agora você tem na mão.
  • Instale a lâmina no cortador de grama.
  • Cortar a grama.

O que acontece? Você dorme para sempre porque agora a operação de verificar o correio está bloqueada em algo que acontece depois que o correio chega .

É extremamente fácil entrar nessa situação quando você espera de forma síncrona uma tarefa arbitrária. Essa tarefa pode ter um trabalho agendado no futuro do encadeamento que está aguardando e agora esse futuro nunca chegará porque você está esperando por ele.

Se você fizer uma espera assíncrona , tudo estará bem! Você verifica periodicamente o correio e, enquanto espera, faz um sanduíche ou paga seus impostos ou o que for; você continua fazendo o trabalho enquanto espera.

Nunca espere de forma síncrona. Se a tarefa for concluída, é desnecessária . Se a tarefa não for concluída, mas agendada para executar o encadeamento atual, será ineficiente porque o encadeamento atual pode estar atendendo outro trabalho em vez de aguardar. Se a tarefa não for concluída e o agendamento for executado no encadeamento atual, ele ficará suspenso para aguardar síncrona. Não há um bom motivo para esperar de forma síncrona, novamente, a menos que você já saiba que a tarefa está concluída .

Para ler mais sobre este tópico, consulte

https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html

Stephen explica o cenário do mundo real muito melhor do que eu.


Agora vamos considerar a "outra direção". Podemos compartilhar código fazendo a versão assíncrona simplesmente fazer a versão síncrona em um thread de trabalho?

Essa é possivelmente e de fato provavelmente uma má ideia, pelas seguintes razões.

  • É ineficiente se a operação síncrona for um trabalho de E / S de alta latência. Isso contrata essencialmente um trabalhador e o faz dormir até que uma tarefa seja concluída. Threads são incrivelmente caros . Eles consomem um milhão de bytes de espaço de endereço mínimo por padrão, levam tempo, consomem recursos do sistema operacional; você não quer queimar um fio fazendo um trabalho inútil.

  • A operação síncrona pode não ter sido gravada como segura para threads.

  • Essa é uma técnica mais razoável se o trabalho de alta latência estiver vinculado ao processador, mas se for, provavelmente você não deseja simplesmente entregá-lo a um encadeamento de trabalho. Você provavelmente deseja usar a biblioteca paralela de tarefas para paralelá-la ao maior número possível de CPUs, provavelmente deseja lógica de cancelamento e não pode simplesmente fazer com que a versão síncrona faça tudo isso, porque já seria a versão assíncrona .

Leitura adicional; novamente, Stephen explica muito claramente:

Por que não usar o Task.Run:

https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-using.html

Mais cenários de "faça e não faça" para o Task.Run:

https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-dont-use.html


O que isso nos deixa então? Ambas as técnicas para compartilhar código levam a impasses ou grandes ineficiências. A conclusão a que chegamos é que você deve fazer uma escolha. Deseja um programa eficiente, correto e que encante o chamador, ou deseja salvar algumas pressionamentos de tecla, duplicando uma pequena quantidade de código entre os caminhos síncrono e assíncrono? Você não recebe os dois, receio.

Eric Lippert
fonte
Você pode explicar por que o retorno Task.Run(() => SynchronousMethod())na versão assíncrona não é o que o OP deseja?
Guillaume Sasdy
2
@GuillaumeSasdy: O pôster original respondeu à pergunta sobre o que é o trabalho: ele está vinculado ao IO. É um desperdício executar tarefas vinculadas de E / S em um encadeamento de trabalho!
Eric Lippert
Tudo bem, eu entendo. Acho que você está certo, e também a resposta @StriplingWarrior explica ainda mais.
Guillaume Sasdy
2
O @Marko praticamente qualquer solução alternativa que bloqueie o encadeamento acabará (sob alta carga) consumindo todos os encadeamentos do conjunto de encadeamentos (que geralmente são usados ​​para concluir operações assíncronas). Como resultado, todos os threads estarão aguardando e nenhum estará disponível para permitir que as operações executem parte do código "operação assíncrona concluída". A maioria das soluções alternativas é boa em cenários de baixa carga (como existem muitos encadeamentos, até 2-3 são bloqueados como parte de cada operação) ... E se você puder garantir que a conclusão da operação assíncrona seja executada no novo encadeamento do SO (não no conjunto de encadeamentos), pode até funcionar para todos os casos (você paga um preço alto)
Alexei Levenkov
2
Às vezes, não há outra maneira senão "simplesmente fazer a versão síncrona em um encadeamento de trabalho". Por exemplo, é assim que Dns.GetHostEntryAsyncé implementado no .NET e FileStream.ReadAsyncem certos tipos de arquivos. O sistema operacional simplesmente não fornece interface assíncrona, portanto o tempo de execução precisa ser falsificado (e não é específico do idioma - por exemplo, o tempo de execução Erlang executa uma árvore inteira de processos de trabalho , com vários threads dentro de cada um, para fornecer E / S e nome de disco sem bloqueio resolução).
Joker_vD 8/01
6

É difícil dar uma resposta única para essa pergunta. Infelizmente, não existe uma maneira simples e perfeita de obter reutilização entre código assíncrono e síncrono. Mas aqui estão alguns princípios a considerar:

  1. Código assíncrono e síncrono geralmente são fundamentalmente diferentes. Código assíncrono geralmente deve incluir um token de cancelamento, por exemplo. E muitas vezes acaba chamando métodos diferentes (como seu exemplo chama Query()em um e QueryAsync()no outro) ou configurando conexões com configurações diferentes. Portanto, mesmo quando estruturalmente semelhante, muitas vezes há diferenças de comportamento suficientes para merecê-las sendo tratadas como código separado com requisitos diferentes. Observe as diferenças entre as implementações de métodos Async e Sync na classe File, por exemplo: nenhum esforço é feito para fazê-los usar o mesmo código
  2. Se você estiver fornecendo uma assinatura de método assíncrona para implementar uma interface, mas tiver uma implementação síncrona (ou seja, não há nada inerentemente assíncrono no que seu método faz), você pode simplesmente retornar Task.FromResult(...) .
  3. Quaisquer peças síncronos de lógica, que são o mesmo entre os dois métodos pode ser extraído a um método auxiliar separado e aproveitados em ambos os métodos.

Boa sorte.

StriplingWarrior
fonte
-2

Fácil; faça com que o síncrono chame o assíncrono. Existe até um método útil Task<T>para fazer exatamente isso:

public class Foo
{
   public bool IsIt()
   {
      var task = IsItAsync(); //no await here; we want the Task

      //Some tasks end up scheduled to run before you get them;
      //don't try to run them a second time
      if((int)task.Status > (int)TaskStatus.Created)
          //this call will block the current thread,
          //and unlike Run()/Wait() will prefer the current 
          //thread's TaskScheduler instead of a new thread.
          task.RunSynchronously(); 

      //if IsItAsync() can throw exceptions,
      //you still need a Wait() call to bring those back from the Task
      try{ 
          task.Wait();
          return task.Result;
      }
      catch(Exception ex) 
      { 
          //Handle IsItAsync() exceptions here;
          //remember to return something if you don't rethrow              
      }
   }

   public async Task<bool> IsItAsync()
   {
      // Some async logic
   }
}
KeithS
fonte
11
Veja minha resposta para saber por que essa é a pior prática que você nunca deve fazer.
Eric Lippert
11
Sua resposta lida com GetAwaiter (). GetResult (). Eu evitei isso. A realidade é que, às vezes, você precisa executar um método de forma síncrona para usá-lo em uma pilha de chamadas que não pode ser assíncrona. Se a espera pela sincronização nunca deve ser feita, então como um (ex) membro da equipe C #, por favor, diga-me por que você não colocou uma, mas duas maneiras de aguardar de forma síncrona uma tarefa assíncrona no TPL.
KeithS
A pessoa a fazer essa pergunta seria Stephen Toub, não eu. Eu só trabalhei no lado da linguagem das coisas.
Eric Lippert