Programação
assíncrona com C# 5
André Bires @ Take.net
Janeiro de 2014
Il Pizzeria Ritardato
Pizzeria Ritardato
5 garçons
10 mesas
Pizzeria Ritardato
Garçom espera na
cozinha o pedido
Pizzeria Ritardato
+ de 5 pedidos =
Clientes esperando
Pizzeria Ritardato
Contratação de + 5
garçons
Total = 10
Pizzeria Ritardato
Custo > Receita =
Prejuízos
Il Pizzeria Veloce
Pizzeria Veloce
3 garçons
10 mesas
Pizzeria Veloce
Garçom leva
pedido na cozinha
e volta para o
balcão
Pizzeria Veloce
Campainha
Garçom pedido !=
garçom entrega
Pizzeria Veloce
Aumento da
freguesia
3 garçons + que
suficientes
Ampliação para 15
mesas
Pizzeria Veloce
Mesmo custo com
Maior receita
=
Maior eficiência
?
!
Programação assíncrona
Uso eficiente dos recursos computacionais
NUNCA
bloquear uma
thread
Thread
bloqueada =
Dinheiro
jogado fora
Thread
bloqueada =
Destruição
da natureza
Programação assíncrona
Retorno ao threadpool
Notificação de conclusão
Não é o mesmo
que programação
paralela!
Programação assíncrona
Pizzaria: Servidor
Mesa/cliente: Aplicação
Garçom: Thread
Salário do garçom: Memória da thread
Balcão: Thread pool
Programação assíncrona
Pedido: Requisição externa
Pizzaiolo: Serviço externo
Pizza: Resultado da requisição
Campainha: Notificação
Programação assíncrona com C# 5
Programação assíncrona com C#
Asynchronous Programming Model (APM)
Programação assíncrona com C#
Asynchronous Programming Model (APM)
Event-based Asynchronous Pattern (EAP)
Programação assíncrona com C#
Asynchronous Programming Model (APM)
Event-based Asynchronous Pattern (EAP)
Task-based Asynchronous Pattern (TAP)
Programação assíncrona com C#
Asynchronous Programming Model (APM)
Event-based Asynchronous Pattern (EAP)
Task-based Asynchronous Pattern (TAP)
Task-based Asynchronous Pattern
Task ou Task<T>
C# 4
Task = Promisse ou Future
Task-based Asynchronous Pattern
() => { }
Threadpool
Task-based Asynchronous Pattern
private void Example1Button_Click(object sender, EventArgs e) 
{
     toolStripStatusLabel.Text = "Calling the API...";
     Task<string> task = Task.Run(() => GetName(1));
     nameTextBox.Text = task.Result;
     toolStripStatusLabel.Text = "Done"; 
}
Task-based Asynchronous Pattern
private void Example1Button_Click(object sender, EventArgs e) 
{
     toolStripStatusLabel.Text = "Calling the API...";
     Task<string> task = Task.Run(() => GetName(1));
     nameTextBox.Text = task.Result;
     toolStripStatusLabel.Text = "Done"; 
}
Task-based Asynchronous Pattern
ContinueWith
Task-based Asynchronous Pattern
private void Example2Button_Click(object sender, EventArgs e) 
{
     toolStripStatusLabel.Text = "Calling the API...";
     Task.Run(() => GetName(1))
     .ContinueWith(t =>
         this.Invoke(new Action(() =>
             {
                 nameTextBox.Text = t.Result;
                 toolStripStatusLabel.Text = "Done";
             }))
     );
}
Task-based Asynchronous Pattern
private void Example2Button_Click(object sender, EventArgs e) 
{
     toolStripStatusLabel.Text = "Calling the API...";
     Task.Run(() => GetName(1))
     .ContinueWith(t =>
         this.Invoke(new Action(() =>
             {
                 nameTextBox.Text = t.Result;
                 toolStripStatusLabel.Text = "Done";
             }))
     );
}
Task-based Asynchronous Pattern
private void Example2Button_Click(object sender, EventArgs e) 
{
     toolStripStatusLabel.Text = "Calling the API...";
     Task.Run(() => GetName(1))
     .ContinueWith(t =>
         this.Invoke(new Action(() =>
             {
                 nameTextBox.Text = t.Result;
                 toolStripStatusLabel.Text = "Done";
             }))
     );
}
Task-based Asynchronous Pattern
private void Example2Button_Click(object sender, EventArgs e) 
{
     toolStripStatusLabel.Text = "Calling the API...";
     Task.Run(() => GetName(1))
     .ContinueWith(t =>
         this.Invoke(new Action(() =>
             {
                 nameTextBox.Text = t.Result;
                 toolStripStatusLabel.Text = "Done";
             }))
     );
}
Task-based Asynchronous Pattern
private void Example1Button_Click(object sender, EventArgs e) 
{
     toolStripStatusLabel.Text = "Calling the API...";
     for (int i = 0; i < repeatNumericUpDown.Value; i++)
     {
         Task<string> task = Task.Run(() => GetName(1));
         nameTextBox.AppendText(task.Result);
         nameTextBox.AppendText(Environment.NewLine);
     }
     toolStripStatusLabel.Text = "Done"; 
}
private void Example2Button_Click(object sender, EventArgs e) 
{
     toolStripStatusLabel.Text = "Calling the API...";
     GetNames(Convert.ToInt32(repeatNumericUpDown.Value));
} 
private void GetNames(int remaining) 
{
     Task.Run(() => GetName(1))
         .ContinueWith(t =>
             this.Invoke(new Action(() =>
                 {
                     nameTextBox.AppendText(t.Result);
                     nameTextBox.AppendText(Environment.NewLine);
                     remaining--;
                     if (remaining > 0)
                     {
                         GetNames(remaining);
                     }
                     else
                     {
                         toolStripStatusLabel.Text = "Done";
                     }
                  })
             )
         ); 
}
Task-based Asynchronous Pattern
C# 5.0 (.NET 4.5)
async
await
Task-based Asynchronous Pattern
Método I/O que retorne uma Task
Event handler assíncrono
Task-based Asynchronous Pattern
private async void Example3Button_Click(object sender, EventArgs e) 
{
     toolStripStatusLabel.Text = "Calling the API...";
     nameTextBox.Text = await GetNameAsync(1);
     toolStripStatusLabel.Text = "Done"; 
}
Task-based Asynchronous Pattern
private async void Example3Button_Click(object sender, EventArgs e) 
{
     toolStripStatusLabel.Text = "Calling the API...";
     for (int i = 0; i < repeatNumericUpDown.Value; i++)
     {
         nameTextBox.AppendText(await GetNameAsync(1));
         nameTextBox.AppendText(Environment.NewLine);
     }
     toolStripStatusLabel.Text = "Done"; 
}
Task-based Asynchronous Pattern
private async void Example3Button_Click(object sender, EventArgs e) 
{
     toolStripStatusLabel.Text = "Calling the API...";
     nameTextBox.Text = await GetNameAsync(1);
     toolStripStatusLabel.Text = "Done"; 
}
Task-based Asynchronous Pattern
private async void Example3Button_Click(object sender, EventArgs e) 
{
     toolStripStatusLabel.Text = "Calling the API...";
     nameTextBox.Text = await GetNameAsync(1);
     toolStripStatusLabel.Text = "Done"; 
}
Task-based Asynchronous Pattern
private async void Example3Button_Click(object sender, EventArgs e) 
{
     toolStripStatusLabel.Text = "Calling the API...";
     nameTextBox.Text = await GetNameAsync(1);
     toolStripStatusLabel.Text = "Done"; 
}

Primeiro estado
Segundo estado
Task-based Asynchronous Pattern
Overhead
!
Task-based Asynchronous Pattern
API verdadeiramente assíncronas =
Completion port (Windows)
Task-based Asynchronous Pattern
TcpClient
HttpClient
Socket
Etc.
Task-based Asynchronous Pattern
Async wrappers = EVIL
Task-based Asynchronous Pattern
private Task<string> GetNameAsyncWrapper(int id)
{
     return Task.Run(() =>
     {
         return GetName(id);
     });
} 
private string GetName(int id) 
{
     // Synchronous implemementation 
}
Task-based Asynchronous Pattern
private Task<string> GetNameAsyncWrapper(int id)
{
     return Task.Run(() =>
     {
         return GetName(id);
     });
} 
private string GetName(int id) 
{
     // Synchronous implemementation 
}
Task-based Asynchronous Pattern
private async Task<string> GetNameAsync(int id)
{
    string url = string.Format("http://localhost:3803/names/{0}", id);
 
    using (var client = new HttpClient())
    {
        var httpResponseMessage = await client.GetAsync(url);
 
        if (httpResponseMessage.IsSuccessStatusCode)
        {
            return await httpResponseMessage.Content
                .ReadAsStringAsync();
        }
        else
        {
            return httpResponseMessage.ReasonPhrase;
        }
    }
}
Task-based Asynchronous Pattern
Código legado requer muitas alterações
async
=
infecçã
o
Task-based Asynchronous Pattern
public string Get(int id, bool delay = false)
{
     if (delay)
     {
         // Simula uma chamada externa, como banco de dados
         Thread.Sleep(3000);
     }
     return string.Format(
         "{0} {1}",
         _firstNames[_random.Next(_firstNames.Length - 1)],
         _lastNames[_random.Next(_lastNames.Length - 1)]); 
}
Task-based Asynchronous Pattern
public async Task<string> GetAsync(int id, bool delay = false) 
{
     if (delay)
     {
         // Simula uma chamada externa, como banco de dados
         await Task.Delay(3000);
     }
     return string.Format(
         "{0} {1}",
         _firstNames[_random.Next(_firstNames.Length - 1)],
         _lastNames[_random.Next(_lastNames.Length - 1)]); 
}
Task-based Asynchronous Pattern
Boas práticas:
async void apenas em event handlers
ConfigureAwait(false) em bibliotecas
Task-based Asynchronous Pattern
Callstack é diferente do código síncrono
?

Programação assíncrona com c sharp

Notas do Editor

  • #4 Nesta pizzaria, haviam cinco garçons e dez mesas
  • #5 Sempre que havia um pedido em uma mesa, o garçom o anotava, ia até a cozinha, o entregava ao pizzaiolo e ficava lá, esperando a pizza assar Depois de pronta, o garçom saía da cozinha e entregava a pizza na mesa e por fim, voltava ao balcão para esperar novos pedidos
  • #6 O gerente percebeu que se houvessem mais que cinco pedidos sendo preparados, os próximos clientes não conseguiam fazer seus pedidos e iam embora Como ele era muito esperto, resolveu contratar mais cinco garçons para resolver o problema Desta forma, havia um garçom para cada mesa
  • #7 Como ele era muito esperto, resolveu contratar mais cinco garçons para resolver o problema Desta forma, havia um garçom para cada mesa
  • #8 Mas devido a contratação dos novos garçons, o custo operacional da pizzaria subiu muito, gerando prejuízos no final do mês Para cobrir estes gastos, o gerente resolveu aumentar o preço das pizzas, o que espantou a clientela, que passou a frequentar a Pizzeria Veloce do outro lado da rua A Pizzeria Ritardato fechou pouco tempo depois
  • #10 Na Pizzeria Veloce, haviam três garçons e dez mesas
  • #11 Sempre que havia um pedido em uma mesa, o garçom o anotava, ia até a cozinha, entregava ao pizzaiolo e voltava imediatamente ao balcão para esperar novos pedidos
  • #12 Quando uma pizza estava pronta, o pizzaiolo tocava uma campainha Um garçom que não estivesse ocupado ia até a cozinha, pegava a pizza e entregava na mesa que fez o pedido Não era necessariamente o mesmo garçom que anotava e entregava o pedido em uma mesa
  • #13 Com o fechamento da Pizzeria Ritardato, o movimento de cliente da Pizzeria Veloce aumentou O gerente observou que da forma que trabalhavam, três garçons eram mais que o suficiente para atender as dez mesas Por isso, resolveu ampliar o espaço para quinze mesas, mantendo o mesmo número de garçons
  • #14 Desta forma, os lucros aumentaram, com o crescimento das vendas sem o aumento do custo com funcionários Assim, a Pizzeria Veloce aumentou sua eficiência, utilizando o mesmo número de recursos para vender mais
  • #15 Por que a Pizzeria Veloce conseguiu ser mais eficiente que a Pizzeria Ritardato?
  • #16 Porque funcionava de forma assíncrona!
  • #17 A principal ideia por traz da programação assíncrona é utilizar de forma eficiente os recursos computacionais
  • #18 Consiste basicamente em não bloquear uma thread enquanto esta espera por um resultado de um processamento lento (I/O)
  • #19 Uma thread bloqueada gasta tempo do processador Tempo do processador = energia elétrica = dinheiro
  • #20 Energia elétrica = natureza A cada Thread.Sleep, duas espécies entram em extinção na Amazônia
  • #21 Ao fazer uma requisição externa, a thread volta para o pool para atender outras requisições Quando o processamento externo termina, uma notificação é gerada para que uma thread (pode ser outra) receba o resultado e continue o processamento
  • #22 A Pizzeria Ritardato usava mais threads que a Veloce e exatamente por isso era menos eficiente O objetivo é otimizar a programação paralela.
  • #23 Na analogia com as pizzarias. Memória total do servidor: Orçamento da Pizzaria
  • #24 Serviços externos: Banco de dados, API HTTP, disco, etc.
  • #26 Existem três padrões possíveis para programação assíncrona no .NET: Asynchronous Programming Model (APM) – Utiliza a interface IAsyncResult, com métodos com prefixo Begin e End para a requisição e resultado. Obsoleto.
  • #27 Event-based Asynchronous Pattern (EAP) – Requer um método para a requisição e um evento para o resultado. Obsoleto.
  • #28 Task-based Asynchronous Pattern (TAP) – Utiliza o tipo Task&lt;T&gt; requer apenas um método. Recomendado.
  • #29 É o padrão recomendado.
  • #30 Baseia-se no uso do tipo Task ou Task&lt;T&gt;, onde T é o resultado do processamento da tarefa. Existe desde o C# 4. Uma Task também é chamada de Promisse ou Future (termo da teoria da computação) – Execução em potencial de uma rotina computacional
  • #31 A rotina a ser executada é passada através de uma expressão lambda. Delegates Action ou Func A execução é feita por uma thread do threadpool do .NET O tamanho do threadpool é limitado pela memória, mas existe um limite por processo
  • #32 Apesar de utilizar duas threads (uma principal e uma para a Task), o exemplo anterior funciona de forma síncrona, pois o processamento da primeira thread é bloqueado na chamada task.Result
  • #33 Interface não é responsiva. Seria melhor se conseguíssemos ser notificados da conclusão da tarefa e só depois disso processar o resultado.
  • #34 A classe Task permite o encadeamento de tarefas através da definição da continuação, pelo método ContinueWith. Desta forma, é possível evitar o bloqueio da thread principal enquanto se espera o resultado.
  • #35 Se for uma aplicação gráfica, a thread da interface não fica bloqueada pela ação definida na tarefa.
  • #36 Mas se o método GetName estiver realizando alguma operação de I/O, ainda existe o bloqueio da thread que está executando a tarefa (do threadpool), o que torna a aplicação ineficiente.
  • #37 Um outro problema é que o encadeamento de continuações tende a deixar o código confuso.
  • #38 E por último, é necessário sincronizar o contexto manualmente em aplicações gráficas, já que o ContinueWith é executado por uma thread do pool, e por isso, não tem acesso aos componentes da interface. E se fossem para buscar vários nomes?
  • #39 No exemplo 1 ficaria assim
  • #40 No exemplo 2, seria necessário recursão para garantir que as tarefas acontecam em sequencia.
  • #41 Com o objetivo de simplificar o desenvolvimento deste tipo de aplicação, o C# 5.0 incluiu duas novas palavras-chave: async e await
  • #42 Marcador que permite o uso de outra palavra-chave no método -&gt; AWAIT
  • #43 É aqui onde a mágica acontece. Permite “esperarmos” SEM BLOQUEIO a conclusão de uma Task Como resolver os problemas do nosso exemplo com estas palavras-chave?
  • #44 Uma implementação assíncrona do método GetName, que utilize uma API assíncrona do .NET (por exemplo, HttpClient) e retorne uma Task Alterar o event handler do botão, incluindo a palavra-chave async, que nos permite utilizar a outra palavra-chave await
  • #45 Implementação limpa e mais simples.
  • #46 Versão com repetição do mesmo código
  • #47 O que a palavra-chave async faz? Basicamente, ela nos permite usar no corpo do método a palavra-chave await
  • #48 O compilador busca nos métodos marcados como async pelo uso da palavra-chave await. Estes pontos são como “quebras de página” de um texto, onde os métodos são separados em várias rotinas, representadas dentro de uma máquina de estados.
  • #49 Esta máquina recebe as notificações de conclusão das Tasks avança um estado sempre que isso ocorre. Para o desenvolvedor, isso é transparente: A impressão é que tudo acontece de forma linear, mas na prática, o código gerado pelo compilador é complexo e adiciona um certo overhead à execução.
  • #50 Código refatorado pelo compilador.
  • #51 Máquina de estados gerada para o método.
  • #52 Não valeria a pena, na perspectiva do uso de recursos, utilizar programação assíncrona com C#, devido ao overhead gerado para coordenar a execução deste tipo de código, se não houvesse o ganho que as notificações que estas APIs provêm.
  • #53 APIs verdadeiramente assíncronas utilizam Completion Ports do sistema operacional para receber as notificações do resultado do processamento de uma requisição de I/O. Se não houvesse este recurso, seria necessário uma outra thread para realizar o polling e monitorar o resultado, o que tornaria o processo assíncrono tão ineficiente quanto a programação síncrona
  • #54 As classes do .NET que expõem uma API assíncrona (TcpClient, HttpClient, etc.) contam com este recurso. Ou implemente a sua utilizando o TaskCompletionSource
  • #55 Por este motivo, deve-se evitar utilizar async wrappers, a não ser que o objetivo seja apenas deixar a interface gráfica (UI) mais responsiva
  • #56 Wrapper para um método síncrono não traz ganhos de eficiência. São úteis apenas em interfaces gráficas, mas se houver uma implementação assíncrona, deve ser preferida.
  • #58 Implementação assíncrona que usa os métodos GetAsync e ReadAsStringAsync do .NET Métodos assíncronos sempre retornam Task e tem um sufixo Async
  • #59 Não adianta transformar apenas parte do seu código em assíncrono. Se houver qualquer bloqueio, o desempenho pode ser pior do que a versão síncrona, devido ao overhead.
  • #60 Toda as APIs que fazem IO e bloqueiam envolvidas na chamada devem ser assíncronas. Todo o callstack da chamada deve ser transformado.
  • #61 Metodo síncrono do Web API
  • #62 Versão assíncrono do Web API Basta retornar um Task&lt;T&gt; Post-fixo Async no nome do método
  • #63 Retornar sempre Task ou Task&lt;T&gt;. Não usar async void em métodos. ConfigureAwait(false) evita a captura do contexto da requisição para a continuação, para evitar problemas com deadlocks.
  • #64 Como as chamadas não ocorrem de forma linear, não existe um callstack disponível semelhante a do código síncrono, o que dificulta o debug do código legado. O ideal é logar todas as chamadas.