Julho de 2015

Número 7 do Volume 30

Programação Assíncrona: Desenvolvimento de Recuperação Assíncrono

Por Stephen Cleary | Julho de 2015

Quando o Visual Studio Async CTP foi lançado, eu estava em uma posição privilegiada. Eu era o único desenvolvedor de dois aplicativos relativamente pequenos em ambiente intacto que se beneficiaram muito de async e await. Durante esse tempo, vários membros de fóruns do MSDN (inclusive eu) estavam descobrindo, debatendo e implementando várias práticas recomendadas assíncronas. A mais importante dessas práticas foram compiladas em meu artigo de março de 2013 da MSDN Magazine, "Práticas Recomendadas em Programação Assíncrona" (msdn.microsoft.com/magazine/jj991977).

Aplicar async e await em uma base de código existente é um outro tipo de desafio. Pra complicar ainda mais, código de recuperação já é algo confuso. Explicarei aqui algumas técnicas que acho muito úteis ao aplicar async em código de recuperação. Em alguns casos, a introdução de async pode até mesmo afetar o design. Se houver qualquer necessidade de refatoração para separar o código existente em camadas, eu recomendo fazer isso antes de introduzir o async. Para os fins deste artigo, vou pressupor que você está usando uma arquitetura de aplicativo como a na Figura 1.

Figura 1: estrutura de código simples com uma camada de serviço e a camada de lógica de negócios

public interface IDataService
{
  Task<string> GetAsync(int id);
}
public sealed class WebDataService : IDataService
{
  public async Task<string> GetAsync(int id)
  {
    using (var client = new WebClient())
      return await client.DownloadStringTaskAsync(
      "http://www.example.com/api/values/" + id);
  }
}
public sealed class BusinessLogic
{
  private readonly IDataService _dataService;
  public BusinessLogic(IDataService dataService)
  {
    _dataService = dataService;
  }
  public async Task<string> GetFrobAsync()
  {
    // Try to get the new frob id.
    var result = await _dataService.GetAsync(17);
    if (result != string.Empty)
      return result;
    // If the new one isn't defined, get the old one.
    return await _dataService.GetAsync(13);
  }
}

Quando usar Async

A melhor abordagem geral é, antes de mais nada, refletir sobre o que o aplicativo está realmente fazendo. O async se destaca em operações vinculadas a E/S, mas podem haver opções melhores para outros tipos de processamento. Há dois cenários relativamente comuns em que async não é ideal: códigos vinculados à CPU e fluxos de dados.

Se você tiver código vinculado à CPU, considere a classe Parallel ou Parallel LINQ. O async é mais adequado para um sistema baseado em evento, onde não há nenhum código real em execução enquanto uma operação está em andamento. Um código vinculado à CPU em um método async ainda será executado de forma síncrona.

No entanto, você pode tratar o código vinculado à CPU como se fosse assíncrono, aguardando o resultado do Task.Run. Essa é uma boa maneira de enviar trabalho vinculado à CPU para fora do thread da interface do usuário. O código a seguir é um exemplo do uso de Task.Run como uma ponte entre código assíncrono e paralelo:

await Task.Run(() => Parallel.ForEach(...));

Outro cenário em que o async não é a melhor opção é quando seu aplicativo lida com fluxos de dados. Operações assíncronas têm início e final definidos. Por exemplo, um download de um recurso é iniciado quando o recurso é solicitado. E termina quando o download do recurso é concluído. Se os dados de entrada forem mais de um fluxo ou assinatura, então async pode não ser a melhor abordagem. Como exemplo, considere um dispositivo conectado a uma porta serial que pode fornecer dados voluntariamente a qualquer momento.

É possível usar async/await em fluxos de eventos. Serão necessários alguns recursos do sistema para armazenar os dados no buffer conforme eles chegam, até que o aplicativo leia os dados. Se a sua origem for uma assinatura de evento, considere usar Reactive Extensions ou TPL Dataflow. Talvez sejam opções mais naturais que async simples. Rx e Dataflow interoperam perfeitamente com código assíncrono.

O async é certamente a melhor abordagem para uma extensa gama de códigos, mas não para todos. Seguindo este artigo, partirei do princípio que você avaliou a biblioteca paralela de tarefas e Rx/Dataflow e concluiu que async/await é a abordagem mais apropriada.

Transformando código síncrono em assíncrono

Há um procedimento comum para converter código síncrono existente em código assíncrono. É algo muito simples. A ponto de se tornar entediante depois de repetir a operação algumas vezes. No momento que escrevo este artigo, não há suporte para conversão automática de síncrono para assíncrono. No entanto, acredito que esse tipo de transformação de código seja introduzido nos próximos anos.

Este procedimento funciona melhor quando você inicia nas camadas inferiores e faz o caminho em direção aos níveis do usuário. Em outras palavras, começaremos a introduzir async pelos métodos de camada de dados que acessam um banco de dados ou APIs da Web. Em seguida, introduziremos async em seus métodos de serviço, lógica de negócios e, finalmente, na camada do usuário. Se seu código não tiver camadas bem definidas, você ainda poderá convertê-lo para async/await. Mas será um pouco mais difícil.

A primeira etapa é identificar, no nível inferior, a operação naturalmente assíncrona para conversão. Tudo o que for baseado em E/S é um forte candidato para async. Alguns exemplos comuns são consultas e comandos de banco de dados, chamadas de API da Web e acesso a sistemas de arquivos. Muitas vezes, essa operação de nível inferior já tem uma API assíncrona existente.

Se a biblioteca subjacente tiver uma API para async, você só precisa adicionar um sufixo Async (ou um sufixo TaskAsync) no nome do método síncrono. Por exemplo, uma chamada do Entity Framework para First pode ser substituída por uma chamada para FirstAsync. Em alguns casos, convém usar um tipo alternativo. Por exemplo, HttpClient é uma substituição mais amigável para async para WebClient e HttpWebRequest. Em alguns casos, talvez seja necessário atualizar a versão da sua biblioteca. O Entity Framework, por exemplo, passou a ter uma API para async na versão 6.

Veja o código na Figura 1. Este é um exemplo simples com uma camada de serviço e uma breve lógica de negócios. Neste exemplo, há apenas uma operação de nível inferior: recuperar uma cadeia de caracteres de identificador frob de uma API da Web em WebDataService.Get. É o lugar mais lógico para iniciarmos a conversão assíncrona. Nesse caso, o desenvolvedor pode optar por substituir WebClient.DownloadString por WebClient.DownloadStringTaskAsync, ou substituir WebClient por HttpClient, que é mais amigável para async.

A segunda etapa é alterar a chamada de API síncrona para uma chamada de API assíncrona e, em seguida, esperar a tarefa retornada. Quando o código chama um método assíncrono, geralmente é adequado aguardar a tarefa retornada. Neste ponto, o compilador reclamará. O código a seguir causará um erro do compilador com a mensagem: "o operador 'await' só pode ser usado dentro de um método assíncrono. Considere a possibilidade de marcar esse método com o modificador 'async' e alterar seu tipo de retorno para 'Task < string >'":

public sealed class WebDataService : IDataService
{
  public string Get(int id)
  {
    using (var client = new WebClient())
      return await client.DownloadStringTaskAsync(
      "http://www.example.com/api/values/" + id);
  }
}

O compilador orienta você para a próxima etapa. Marque o método como assíncrono e altere o tipo de retorno. Se o tipo de retorno do método síncrono for nulo, o tipo de retorno do método assíncrono deve ser Task. Caso contrário, para qualquer tipo de retorno de método síncrono de T, o tipo de retorno do método assíncrono deve ser Task<T>. Quando você altera o tipo de retorno para Task/Task<T>, você também deve modificar o nome do método para terminar em Async, seguindo as diretrizes do padrão assíncrono baseado em tarefa. O código a seguir mostra o método resultante como um método assíncrono:

public sealed class WebDataService : IDataService
{
  public async Task<string> GetAsync(int id)
  {
    using (var client = new WebClient())
      return await client.DownloadStringTaskAsync(
      "http://www.example.com/api/values/" + id);
  }
}

Antes de avançarmos, verifique o restante desse método para encontrar outras chamadas de API de bloqueio ou síncronas que você possa tornar async. Métodos assíncronos não devem bloquear, por isso esse método deve chamar APIs assíncronas, se estiverem disponíveis. Neste exemplo simples, não há nenhuma outra chamada de bloqueio. No código do mundo real, fique atento para lógica de repetição e resolução otimista de conflitos.

Devo mencionar o Entity Framework em especial. Uma pequena "pegadinha" é o carregamento lento de entidades relacionadas. Isso sempre é feito de maneira síncrona. Se possível, use consultas assíncronas adicionais explícitas ao invés de carregamento lento.

Agora este método está finalmente pronto. Em seguida, passe por todos os métodos que fazem referência a este e siga este procedimento novamente. Nesse caso, WebDataService.Get fazia parte de uma implementação de interface, portanto você deve alterar a interface para permitir implementações assíncronas:

public interface IDataService
{
  Task<string> GetAsync(int id);
}

Em seguida, passe para os métodos de chamada e siga as mesmas etapas. O resultado final será algo semelhante ao código apresentado na Figura 2. Infelizmente, o código não será compilado até que todos os métodos de chamada sejam transformados em async e, em seguida, todos os seus métodos de chamada sejam transformados em async, e assim por diante. Essa natureza em cascata do async é o aspecto cansativo do desenvolvimento de recuperação.

Figura 2: alterar todos os métodos de chamada para async

public interface IDataService
{
  string Get(int id);
}
public sealed class WebDataService : IDataService
{
  public string Get(int id)
  {
    using (var client = new WebClient())
      return client.DownloadString("http://www.example.com/api/values/" + id);
  }
}
public sealed class BusinessLogic
{
  private readonly IDataService _dataService;
  public BusinessLogic(IDataService dataService)
  {
    _dataService = dataService;
  }
  public string GetFrob()
  {
    // Try to get the new frob id.
    var result = _dataService.Get(17);
    if (result != string.Empty)
      return result;
    // If the new one isn't defined, get the old one.
    return _dataService.Get(13);
  }
}

Por fim, o nível da operação assíncrona em sua base de código subirá até que atinja um método que não seja chamado por outros métodos em seu código. Os métodos de nível superior são chamados diretamente por qualquer estrutura que você esteja usando. Algumas estruturas de trabalho, como ASP.NET MVC, permitem diretamente o código assíncrono. Por exemplo, as ações do controlador MVC do ASP.NET podem retornar Task ou Task<T>. Outras estruturas, como o Windows Presentation Foundation (WPF), permitem manipuladores de eventos assíncronos. Assim, um evento de clique em um botão pode ser async void, por exemplo.

Batendo no muro

Conforme o nível de código assíncrono sobe por todo o aplicativo, você pode chegar a um ponto em que parece impossível continuar. Os exemplos mais comuns disso são construções orientadas por objeto, que não formam malha com a natureza funcional do código assíncrono. Construtores, eventos e propriedades têm seus próprios desafios.

Repensar o design geralmente é a melhor solução para desviar desses problemas. Um exemplo comum são os construtores. No código síncrono, um método construtor pode bloquear em E/S. No mundo assíncrono, uma solução é usar um método alocador assíncrono em vez de um construtor. Outro exemplo são as propriedades. Se uma propriedade de forma síncrona bloquear na E/S, essa propriedade deve provavelmente ser um método. Um exercício de conversão assíncrona é excelente para expor esses tipos de problemas de design que surgem em sua base de códigos ao longo do tempo.

Dicas para a transformação

Executar uma transformação de código assíncrono pode assustar um pouco nas primeiras vezes, mas se torna realmente natural após um pouco de prática. Enquanto você se familiariza com a conversão de código síncrono em assíncrono, aqui estão algumas dicas para você usar durante o processo de conversão.

Enquanto você converte seu código, fique atento nas oportunidades de simultaneidade. O código simultâneo assíncrono costuma ser mais curto e simples do que o código simultâneo síncrono. Por exemplo, considere um método que precisa baixar dois recursos diferentes de uma API REST. A versão síncrona do método certamente faria um download e, em seguida, o outro. No entanto, a versão assíncrona pode simplesmente iniciar ambos os downloads e aguardar de maneira assíncrona que ambos sejam concluídos usando Task.WhenAll.

Outro aspecto a se considerar é o cancelamento. Normalmente, os usuários de aplicativos síncronos estão acostumados com a espera. Se a interface do usuário for responsiva na nova versão, pode ser que eles tenham a expectativa de poder cancelar a operação. Um código assíncrono geralmente oferece suporte a cancelamento, a menos que haja alguma outra razão para que não possa. Na maior parte, seu próprio código assíncrono pode dar suporte a cancelamento, simplesmente utilizando um argumento CancellationToken e passando para os métodos assíncronos que ele chama.

Você pode converter qualquer código usando Thread ou BackgroundWorker para usar Task.Run. O Task.Run é muito mais fácil para a composição de Thread ou BackgroundWorker. Por exemplo, é muito mais fácil expressar: "iniciar dois cálculos de plano de fundo e, em seguida, fazer essa outra coisa quando ambos forem concluídos" com o await moderno e Task.Run, do que com as primitivas construções de threading.

Partições verticais

A abordagem descrita até agora funciona bem se você for o único desenvolvedor para seu aplicativo, e não tenha problemas ou solicitações que possam interferir no seu trabalho de conversão assíncrona. Mas isso não é muito realista, não é mesmo?

Se você não tiver o tempo necessário para converter sua base de código inteira para assíncrona de uma vez só, você pode realizar a conversão com uma leve modificação chamada partições verticais. Usando essa técnica, você pode fazer sua conversão assíncrona para determinadas seções de código. Partições verticais são ideais se você quiser apenas "experimentar" código assíncrono.

Para criar uma partição vertical, identifique o código de nível de usuário que você gostaria de tornar assíncrono. Talvez seja o manipulador de eventos para um botão de interface do usuário que salva em um banco de dados (em que você gostaria de manter a interface do usuário responsiva) ou uma solicitação ASP.NET muito usada que faz o mesmo (em que você deseja reduzir os recursos necessários para a solicitação específica). Percorra o código, definindo a árvore de chamadas para o método. Em seguida, você pode iniciar nos métodos de nível inferior e ir transformando pelo caminho ao subir pela árvore.

Sem dúvida, outro código usará os mesmos métodos de nível inferior. Como você não está pronto para tornar todo o código assíncrono, a solução é criar uma cópia do método. E então transformar essa cópia em assíncrona. Dessa forma, a solução ainda pode compilar em cada etapa. Quando você trabalhar seu caminho até o código em nível de usuário, terá criado uma partição vertical de código assíncrono dentro de seu aplicativo. Uma partição vertical com base em nosso código de exemplo ficaria conforme a Figura 3.

Figura 3: usando partições verticais para converter seções de código em assíncronas

public interface IDataService
{
  string Get(int id);
  Task<string> GetAsync(int id);
}
public sealed class WebDataService : IDataService
{
  public string Get(int id)
  {
    using (var client = new WebClient())
      return client.DownloadString("http://www.example.com/api/values/" + id);
  }
  public async Task<string> GetAsync(int id)
  {
    using (var client = new WebClient())
      return await client.DownloadStringTaskAsync(
      "http://www.example.com/api/values/" + id);
  }
}
public sealed class BusinessLogic
{
  private readonly IDataService _dataService;
  public BusinessLogic(IDataService dataService)
  {
    _dataService = dataService;
  }
  public string GetFrob()
  {
    // Try to get the new frob id.
    var result = _dataService.Get(17);
    if (result != string.Empty)
      return result;
    // If the new one isn't defined, get the old one.
    return _dataService.Get(13);
  }
  public async Task<string> GetFrobAsync()
  {
    // Try to get the new frob id.
    var result = await _dataService.GetAsync(17);
    if (result != string.Empty)
      return result;
    // If the new one isn't defined, get the old one.
    return await _dataService.GetAsync(13);
  }
}

Talvez você tenha observado que há ocorrências de duplicação de código com esta solução. Toda a lógica para os métodos síncronos e assíncronos é duplicada, o que não é nada bom. Em um mundo perfeito, a duplicação de código para essa partição vertical seria apenas temporária. O código duplicado existiria apenas em seu controle de origem até que o aplicativo tenha sido completamente convertido. Neste momento, você pode remover todas as APIs síncronas antigas.

No entanto, você não pode fazer isso em todas as situações. Se você estiver desenvolvendo uma biblioteca (até mesmo uma limitada a uso interno), a compatibilidade com versões anteriores é uma questão importante. É possível que você precise manter APIs síncronas por um bom tempo.

Há três respostas possíveis para esta situação. Primeiro, você pode direcionar a adoção de APIs assíncronas. Se sua biblioteca tiver que realizar trabalhos assíncronos, ela deve expor APIs assíncronas. Em segundo lugar, você pode aceitar a duplicação de código como um mal necessário pela compatibilidade com versões anteriores. Essa será uma solução aceitável somente se sua equipe tiver uma autodisciplina excepcional, ou se a restrição de compatibilidade for apenas temporária.

A terceira solução é aplicar um dos códigos descritos aqui. Embora eu não possa recomendar muito um desses códigos, eles podem ser úteis em uma situação de emergência. Como a operação deles é naturalmente assíncrona, cada um desses códigos é orientado a fornecer uma API síncrona para uma operação naturalmente assíncrona, que é um antipadrão muito conhecido e descrito em detalhes em uma postagem no Blog Server & Tools em bit.ly/1JDLmWD.

Os códigos de bloqueio

A abordagem mais simples é simplesmente bloquear a versão assíncrona. Eu recomendo o bloqueio com GetAwaiter().GetResult ao invés de Wait ou Result. Wait e Result encapsularão todas as exceções dentro de uma AggregateException, o que complica o tratamento de erros. O código de exemplo da camada de serviço ficaria parecido com o código na Figura 4, se o código de bloqueio for utilizado.

Figura 4: código da camada de serviço usando o código de bloqueio

public sealed class WebDataService : IDataService
{
  public string Get(int id)
  {
    return GetAsync(id).GetAwaiter().GetResult();
  }
  public async Task<string> GetAsync(int id)
  {
    // This code will not work as expected.
    using (var client = new WebClient())
      return await client.DownloadStringTaskAsync(
      "http://www.example.com/api/values/" + id);
  }
}

Infelizmente, como o comentário indica, esse código, na verdade, não funcionaria. Ele resultaria em um deadlock comum, descrito no meu artigo "Práticas recomendadas na programação assíncrona", que mencionei anteriormente.

É aí que o código se complica. Um teste de unidade normal passará, mas o mesmo código resultará em deadlock se ele for chamado de um contexto de interface do usuário ou do ASP.NET. Se você usar o código de bloqueio, você deve escrever testes de unidade para verificar esse comportamento. O código na Figura 5 usa o tipo AsyncContext da minha biblioteca AsyncEx, que cria um contexto semelhante a um contexto de interface do usuário ou do ASP.NET.

Figura 5: usando o tipo de AsyncContext

[TestClass]
public class WebDataServiceUnitTests
{
  [TestMethod]
  public async Task GetAsync_RetrievesObject13()
  {
    var service = new WebDataService();
    var result = await service.GetAsync(13);
    Assert.AreEqual("frob", result);
  }
  [TestMethod]
  public void Get_RetrievesObject13()
  {
    AsyncContext.Run(() =>
    {
      var service = new WebDataService();
      var result = service.Get(13);
      Assert.AreEqual("frob", result);
    });
  }
}

A unidade assíncrona teste é aprovada, mas o teste de unidade síncrona nunca é concluído. Esse é o clássico problema de deadlock. O código assíncrono captura o contexto atual e tenta continuar, enquanto o wrapper síncrono bloqueia um thread nesse contexto, impedindo a conclusão da operação assíncrona.

Nesse caso, está faltando um ConfigureAwait(false) em nosso código assíncrono. No entanto, o mesmo problema pode ser causado pelo uso do WebClient. O WebClient usa o padrão assíncrono (EAP) mais antigo, baseado em evento, que sempre captura o contexto. Até mesmo se seu código usa ConfigureAwait(false), ocorrerá o mesmo deadlock do código WebClient. Nesse caso, você pode substituir o WebClient com o HttpClient mais amigável para async e fazê-lo funcionar na área de trabalho, conforme a Figura 6.

Figura 6: usando HttpClient com ConfigureAwait(false) para evitar deadlock

public sealed class WebDataService : IDataService
{
  public string Get(int id)
  {
    return GetAsync(id).GetAwaiter().GetResult();
  }
  public async Task<string> GetAsync(int id)
  {
    using (var client = new HttpClient())
      return await client.GetStringAsync(
      "http://www.example.com/api/values/" + id).ConfigureAwait(false);
  }
}

O código de bloqueio requer que sua equipe tenha uma disciplina muito rígida. Eles precisam assegurar o uso de ConfigureAwait(false) em todos os lugares. Eles também devem exigir que todas as bibliotecas dependentes sigam a mesma disciplina. Em alguns casos, isso simplesmente não é possível. Enquanto escrevo este artigo, até mesmo o HttpClient captura o contexto em algumas plataformas.

Outra desvantagem para o código de bloqueio é a exigência que você use ConfigureAwait(false). Caso o código assíncrono precise continuar no contexto capturado, ele será simplesmente inadequado. Se você adotar o código de bloqueio, é extremamente recomendado que você execute testes de unidade usando AsyncContext ou outro contexto de thread único semelhante para descobrir qualquer risco de deadlock.

O código de pool de threads

Uma abordagem semelhante ao código de bloqueio é descarregar o trabalho assíncrono para o pool de threads e, em seguida, bloquear a tarefa resultante. Esse código seria semelhante ao na Figura 7.

Figura 7: código para o pool de threads

public sealed class WebDataService : IDataService
{
  public string Get(int id)
  {
    return Task.Run(() => GetAsync(id)).GetAwaiter().GetResult();
  }
  public async Task<string> GetAsync(int id)
  {
    using (var client = new WebClient())
      return await client.DownloadStringTaskAsync(
      "http://www.example.com/api/values/" + id);
  }
}

A chamada para Task.Run executa o método assíncrono em um segmento do pool de threads. Aqui, ele será executado sem um contexto, evitando assim o deadlock. Um dos problemas dessa abordagem é que o método assíncrono não pode depender de execução em um contexto específico. Portanto, ele não pode usar elementos da interface do usuário ou do HttpContext.Current do ASP.NET.

Outra "pegadinha" mais sutil é que o método assíncrono pode continuar em qualquer thread do pool de threads. Isso não é problema para a maioria dos códigos. Mas pode ser um problema se o método usa o estado de cada thread ou implicitamente depende da sincronização fornecida por um contexto da interface do usuário.

Você pode criar um contexto para um thread em segundo plano. O tipo AsyncContext na minha biblioteca AsyncEx instalará um contexto de thread único completo com um "loop principal". Isso forçará o código assíncrono a continuar no mesmo thread. Isso evita as "pegadinhas" mais sutis do código do pool de threads. O exemplo de código com um loop principal para o pool de threads ficaria como na Figura 8.

Figura 8: usando um loop principal para o ataque de pool de threads

public sealed class WebDataService : IDataService
{
  public string Get(int id)
  {
    var task = Task.Run(() => AsyncContext.Run(() => GetAsync(id)));
    return task.GetAwaiter().GetResult();
  }
  public async Task<string> GetAsync(int id)
  {
    using (var client = new WebClient())
      return await client.DownloadStringTaskAsync(
      "http://www.example.com/api/values/" + id);
  }
}

É claro, também há uma desvantagem dessa abordagem. O pool de threads é bloqueado dentro de AsyncContext até que o método assíncrono seja concluído. Esse thread bloqueado estará lá, bem como o thread primário chamando a API síncrona. Assim, pela duração da chamada, existirão dois threads sendo bloqueados. No ASP.NET em particular, essa abordagem reduzirá significativamente a capacidade de dimensionamento do aplicativo.

O código de argumento do sinalizador

Ainda não usei esse código. Ele foi descrito para mim por Stephen Toub durante sua revisão técnica deste artigo. É uma ótima abordagem e minha favorita entre todos esses códigos.

O código de argumento do sinalizador usa o método original, torna-o particular e adiciona um sinalizador para indicar se o método deve ser executado de forma síncrona ou assíncrona. Em seguida, ele expõe duas APIs públicas, uma síncrona, e a outra, assíncrona, como na Figura 9.

Figura 9: o código de argumento do sinalizador expõe duas APIs

public interface IDataService
{
  string Get(int id);
  Task<string> GetAsync(int id);
}
public sealed class WebDataService : IDataService
{
  private async Task<string> GetCoreAsync(int id, bool sync)
  {
    using (var client = new WebClient())
    {
      return sync
        ? client.DownloadString("http://www.example.com/api/values/" + id)
        : await client.DownloadStringTaskAsync(
        "http://www.example.com/api/values/" + id);
    }
  }
  public string Get(int id)
  {
    return GetCoreAsync(id, sync: true).GetAwaiter().GetResult();
  }
  public Task<string> GetAsync(int id)
  {
    return GetCoreAsync(id, sync: false);
  }
}

O método GetCoreAsync neste exemplo tem uma propriedade importante: se seu argumento de sincronização for true, ele sempre retorna uma tarefa já concluída. O método irá bloquear quando o argumento do sinalizador dele solicitar um comportamento síncrono. Caso contrário, ele atua como um método assíncrono normal.

O wrapper de Get síncrono informa true para o argumento do sinalizador e, em seguida, recupera o resultado da operação. Observe que não há nenhuma chance de ocorrer deadlock, porque a tarefa já foi concluída. A lógica de negócios segue um padrão semelhante, conforme a Figura 10.

Figura 10: aplicando o código de argumento do sinalizador na lógica de negócios

public sealed class BusinessLogic
{
  private readonly IDataService _dataService;
  public BusinessLogic(IDataService dataService)
  {
    _dataService = dataService;
  }
  private async Task<string> GetFrobCoreAsync(bool sync)
  {
    // Try to get the new frob id.
    var result = sync
      ? _dataService.Get(17)
      : await _dataService.GetAsync(17);
    if (result != string.Empty)
      return result;
    // If the new one isn't defined, get the old one.
    return sync
      ? _dataService.Get(13)
      : await _dataService.GetAsync(13);
  }
  public string GetFrob()
  {
    return GetFrobCoreAsync(sync: true).GetAwaiter().GetResult();
  }
  public Task<string> GetFrobAsync()
  {
    return GetFrobCoreAsync(sync: false);
  }
}

Você tem a opção de expor os métodos CoreAsync da sua camada de serviço. Isso simplifica a lógica de negócios. No entanto, o método de argumento do sinalizador é mais um detalhe de implementação. Você precisa avaliar a vantagem de um código mais simples em relação à desvantagem de expor detalhes de implementação, conforme a Figura 11. A vantagem desse código é que a lógica dos métodos basicamente permanece a mesma. Ele simplesmente chama APIs diferentes com base no valor do argumento do sinalizador. Isso funciona muito bem se houver uma correspondência direta entre APIs síncronas e assíncronas, o que normalmente é o caso.

Figura 11: detalhes de implementação são expostos, mas o código é limpo

public interface IDataService
{
  string Get(int id);
  Task<string> GetAsync(int id);
  Task<string> GetCoreAsync(int id, bool sync);
}
public sealed class BusinessLogic
{
  private readonly IDataService _dataService;
  public BusinessLogic(IDataService dataService)
  {
    _dataService = dataService;
  }
  private async Task<string> GetFrobCoreAsync(bool sync)
  {
    // Try to get the new frob id.
    var result = await _dataService.GetCoreAsync(17, sync);
    if (result != string.Empty)
      return result;
    // If the new one isn't defined, get the old one.
    return await _dataService.GetCoreAsync(13, sync);
  }
  public string GetFrob()
  {
    return GetFrobCoreAsync(sync: true).GetAwaiter().GetResult();
  }
  public Task<string> GetFrobAsync()
  {
    return GetFrobCoreAsync(sync: false);
  }
}

Pode não funcionar bem se você deseja adicionar simultaneidade ao seu caminho de código assíncrono, ou se não houver uma API assíncrona correspondente ideal. Por exemplo, prefiro usar HttpClient do que WebClient em WebDataService, mas eu teria que levar em conta a complexidade adicional que ele causa no método GetCoreAsync.

A principal desvantagem desse código é que o argumentos do sinalizador são um antipadrão conhecido. Argumentos de sinalizador booliano são um bom indicador de que um método é, na verdade, dois métodos diferentes em um. No entanto, o antipadrão é minimizado dentro os detalhes da implementação de uma única classe (ao menos que você prefira expor seus métodos CoreAsync). Apesar disso, ainda é meu código favorito.

O código de loop de mensagens aninhado

Esse código final é o que gosto menos. A idéia é configurar um loop de mensagens aninhado dentro do thread da interface do usuário e executar o código assíncrono dentro do loop. Essa abordagem não é uma opção no ASP.NET. Ela também pode exigir um código diferente para várias plataformas de interface do usuário. Por exemplo, um aplicativo de WPF pode usar quadros de dispatcher aninhado, enquanto um aplicativo Windows Forms pode usar DoEvents dentro de um loop. Se os métodos assíncronos não dependem de uma determinada plataforma de interface do usuário, você também pode usar AsyncContext para executar um loop aninhado, como na Figura 12.

Figura 12: executando uma mensagem aninhada com asynccontext

public sealed class WebDataService : IDataService
{
  public string Get(int id)
  {
    return AsyncContext.Run(() => GetAsync(id));
  }
  public async Task<string> GetAsync(int id)
  {
    using (var client = new WebClient())
      return await client.DownloadStringTaskAsync(
      "http://www.example.com/api/values/" + id);
  }
}

Não se engane pela simplicidade desse código de exemplo. Esse código é o mais perigoso de todos, porque você deve considerar a reentrada. Isso é especialmente verdadeiro se o código usa quadros de dispatcher aninhados ou DoEvents. Nesse caso, toda a camada de interface do usuário agora precisa lidar com a reentrância inesperada. Aplicativos seguros na reentrada requerem uma quantidade considerável de raciocínio e planejamento.

Conclusão

Em um mundo ideal, você poderia executar uma transformação de código relativamente simples de síncrono para assíncrono em meio a arco-íris e unicórnios. No mundo real, geralmente é necessário que o código síncrono e assíncrono coexistam. Se você deseja apenas experimentar o async, crie uma partição vertical (com duplicação de código) até que você se familiarize com o uso de async. Se você precisa manter o código síncrono por razões de compatibilidade com versões anteriores, você terá que conviver com a duplicação de código ou aplicar um dos códigos.

Um dia, operações assíncronas só serão representadas com APIs assíncronas. Até lá, você precisa viver no mundo real. Espero que essas técnicas te ajudem a adotar async em seus aplicativos existentes da melhor maneira possível.


Stephen Cleary* é marido, pai e programador, e mora no norte de Michigan. Ele trabalha com multithreading e programação assíncrona há 16 anos e tem usado o suporte assíncrono no Microsoft .NET Framework desde o primeiro CTP. Siga seus projetos e postagens de blog em stephencleary.com.*

Agradecemos aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo: James McCaffery e Stephen Toub