C# - Programação Assincrona usando async e await


A programação assíncrona tem sido um território onde poucos se atrevem a se aventurar pois até o momento esse território tem sido muito pantanoso e cheio de areia movediça.

Com o advento da Microsoft .NET Framework 4.5 isso mudou; a partir dessa versão tanto a linguagem C# como a VB .NET permitem que um "programador comum" possa escrever métodos assíncronos da mesma forma que escrevia métodos síncronos.

Todo esforço necessário para contornar os problemas oriundos da programação assíncrona que os programadores tinham que fazer para que os seus métodos assíncronos pudessem funcionar foi abstraído pelos novos recursos.

Você pode apenas usar o recurso sem se preocupar com o que esta ocorrendo nos bastidores ou pode querer entender qual o custo essa abstração vai ter para continuar tendo um controle total sobre o seu código.

Embora as linguagens C# e Visual Basic e os compiladores sejam capazes de fornecer a ilusão de que um método assíncrono é idêntico ao seu correspondente síncrono, as coisas são diferentes nos bastidores.

O compilador acaba gerando muitos códigos em nome do desenvolvedor, códigos que são similares às quantidades de códigos de texto clichê que os desenvolvedores que implementavam assincronicidade  antigamente teriam de escrever e manter manualmente.

Além disso, o código gerado pelo compilador chama um código de biblioteca no .NET Framework, mais uma vez aumentando o trabalho realizado em nome do desenvolvedor.

Neste artigo vamos abordar os conceitos básicos e a utilização de métodos assíncronos no C# e no Visual Basic.

As linguagens de programação Visual Basic e C# são imperativas, isso significa que elas permitem que você expresse a sua lógica de programação como uma sequência de etapas separadas a serem executadas uma após a outra.(Ref2)

Atualmente tanto o C# como o VB .NET oferecem métodos síncronos e assíncronos e por padrão qualquer método que você crie normalmente no seu dia a dia é síncrono.

A seguir temos um exemplo de código síncrono que acessa um banco de dados e preenche um controle TextBox em um formulário com dados. Esse código é totalmente síncrono.

        private void CarregaDados()
        {
            SqlConnection conn = new SqlConnection(@"Data Source=.\sqlexpress;Initial Catalog=Cadastro;Integrated Security=True");
            string sql = @"select Id,Nome from Alunos";
            try{
                SqlCommand cmd = new SqlCommand(sql,conn);
                conn.Open();
                SqlDataReader rdr = cmd.ExecuteReader();
                while (rdr.Read()){
                    txtDados.AppendText("\nId: ");
                    txtDados.AppendText(rdr.GetValue(0) + "\t\t" + rdr.GetValue(1));
                    txtDados.AppendText("\n");
                }
            }
            catch (SqlException ex){
                MessageBox.Show(ex.Message + ex.StackTrace, "Detalhes Exception");
            }
            finally{
               conn.Close();
            }
        }

Na maioria das linguagens imperativas, incluindo as versões atuais do Visual Basic e do C#, a execução dos métodos (ou das funções, ou dos procedimentos) é contínua. Ou seja, uma vez que uma thread de controle comece a executar um determinado método, ela ficará ocupada com essa tarefa até que a execução do método seja concluída.

Às vezes a thread estará executando instruções em métodos chamados pelo seu código, mas isso faz parte da execução do método. A thread nunca fará algo que não foi solicitado pelo seu método.(Ref2)

Às vezes, essa continuidade, esse sincronismo é um problema. Às vezes, não há nada que um método possa fazer para progredir – tudo o que ele pode fazer é esperar que algo aconteça: um download, o acesso a um arquivo, o cálculo executado em uma thread diferente, etc. Nesses casos, a thread fica totalmente ocupada sem fazer nada.

O termo geralmente usado nessas situações é: thread bloqueado; o método que causa isso é conhecido como bloqueador.(Ref2)

Mas o que há de errado com esse comportamento ?

O comportamento síncrono pode deixar os usuários finais com uma má experiência e uma interface bloqueada/congelada sempre que o usuário tentar realizar alguma operação demorada.

Na figura abaixo temos um exemplo clássico onde durante a carga dos dados a interface do usuário fica congelada aguardando o processamento.

Qual a solução para este problema ?

A chamada do método síncrono pode criar um atraso na execução do programa que provoca uma má experiência do usuário. Por isso, uma abordagem assíncrona (threads) seria melhor.

Uma chamada de método assíncrono (criação de uma thread) irá retornar imediatamente para que o programa possa realizar outras operações enquanto o método chamado conclui o seu trabalho em determinadas situações.

O comportamento do método assíncrono é diferente do síncrono, porque um método assíncrono é uma thread separada. Você cria a thread; a thread começa a executar, mas o controle é imediatamente retornado para a thread que a chamou, enquanto a outra thread continua a executar.

Em geral, a programação assíncrona faz sentido em dois cenários:

  1. Se você estiver criando uma aplicação com um interface intensiva na qual a experiência do usuário é a principal preocupação. Neste caso, uma chamada assíncrona permite que a interface do usuário continue a responder e não fique congelada;
  2. Se você tem um trabalho computacional complexo(muitos cálculos) ou muito demorado a fazer, e você tem que continuar interagindo com a interface do usuário do aplicativo enquanto espera a resposta de volta a partir da tarefa de longa duração;

A partir da versão 5.0 da linguagem C# o modificador async/wait e Async e Await no Visual Basic, oferecem uma maneira completamente diferente e fácil de fazer programação assíncrona.

As palavras chave Async e Await em Visual Basic e ascyn e await em C# são o coração de programação assíncrona.

Ao utilizar essas duas palavras-chave, você pode usar os recursos do. NET Framework ou o Windows Runtime para criar um método assíncrono quase tão facilmente como você cria um método síncrono. Os métodos assíncronos que você define usando esses recursos referidos como métodos assíncronos.

Como resultado, o código assíncrono é fácil de implementar e manter a sua estrutura lógica. Por isso agora é tão fácil escrever código assíncrono como escrever o seu método normal, sem preocupação de qualquer canalização extra.

Considere o exemplo usado no início deste artigo onde temos uma interface WPF UI com vinculação de dados, buscando um grande número registros em um banco de dados. Enquanto os dados estão sendo acessados e tratados, o resto da interface do usuário deve continuar a ser responsiva. Qualquer tentativa de interação com outros controles de interface do usuário não devem ser bloqueadas e e o carregamento de dados e sua vinculação com os controles deve continuar em paralelo.

Criando código assíncrono com async e await

Vamos então transformar o código para carregar os dados no formulário de síncrono para assíncrono.

Para criar código assíncrono usamos as palavras chaves async e await onde por padrão um método modificado por uma palavra-chave async contém pelo menos uma expressão await.

O método é executado de forma síncrona até que ele alcance a primeira expressão await, e neste ponto o método é suspenso até que a tarefa seja completada. Enquanto isso , o controle retorna para o chamador do método, como o exemplo neste tópico mostra.

Se um método que esta sendo modificado por uma palavra-chave async não contém uma expressão ou uma instrução await, o método é executado de forma síncrona. Um aviso do compilador o avisa sobre qualquer método assíncrono que não contiver um await porque essa situação pode indicar um erro.

Quando async modifica um método, uma expressão lambda ou um método anônimo, async é uma palavra-chave. Em todos os outros contextos, async é interpretado como um identificador. Esta distinção faz async uma palavra-chave contextual.

Um método async (assíncrono) pode ter um tipo de retorno Task, Task(Of TResult) ou void. O método não pode declarar qualquer parâmetro ref ou out, embora ele pode chamar métodos que tenham esses parâmetros.

  1. Você especifica Task<TResult> como o tipo de retorno de um método assíncrono se a declaração de retorno do método especifica um operando do tipo TResult.
  2. Você usa Task se nenhum valor significativo é retornado quando o método for concluído.
  3. O tipo de retorno void é usado principalmente para definir manipuladores de eventos, onde um tipo de retorno void é necessário.

Em métodos assíncronos, você usa as palavras-chave e tipos para indicar o que você quer fazer, e o compilador faz o resto, incluindo manter o controle do que deve acontecer quando o controle retorna para um ponto await em um método suspenso.

Não pense que você pode sair por ai usando async e await indiscriminadamente. Existem algumas regras básicas que devem ser seguidas. A seguir algumas regras importas para o uso de async e await:

Agora veja como ficou o código do exemplo usando async e await:

    private async void CarregarDadosAssincrono()
        {
            SqlConnection conn = new SqlConnection(
@"Data Source=.\sqlexpress;Initial Catalog=Cadastro;Integrated Security=True");
            string sql = @"select Id,Nome from Alunos";
            try
            {
                SqlCommand cmd = new SqlCommand(sql, conn);
                await conn.OpenAsync();

                using (SqlDataReader rdr = await cmd.ExecuteReaderAsync())
                {
                    while (await rdr.ReadAsync())
                    {
                        txtDados.AppendText("\nId: ");
                        txtDados.AppendText(await rdr.GetFieldValueAsync<int>(0) + "\t\t" + await rdr.GetFieldValueAsync<string>(1));
                        txtDados.AppendText("\n");
                    }
                }
            }
            catch (SqlException ex)
            {
                MessageBox.Show(ex.Message + ex.StackTrace, "Detalhes Exception");
            }
            finally
            {
                conn.Close();
            }
        }

Obs: Naturalmente a utilização do código assíncrono somente se justifica em cenários de grande volume de dados ou complexidade de processamento quando o tempo impacta o desempenho.

Este código além de usar async e await também usa os métodos OpenASync(), ExecuteReaderAsync(), ReadAsync() e GetFieldValueAsync<T>() da versão 4.5.

É importante decidir antes de criar um SqlDataReader se voceê precisa usar o modo de acesso não-sequencial ou sequencial. Na maioria dos casos, utilizar o modo de acesso padrão (não-sequencial) é a melhor opção, pois permite um modelo de programação mais fácil (você pode acessar qualquer coluna em qualquer ordem), e você obterá um melhor desempenho usando ReadAsync.

No entanto, desde que o modo de acesso não sequencial tem de armazenar os dados para toda a linha, ela pode causar problemas se você estiver lendo uma grande coluna do servidor (como varbinary (MAX), varchar (max), nvarchar (MAX) ou XML). Neste caso, usando o modo de acesso seqüencial lhe permitirá transmitir estas grandes colunas ao invés de ter que colocar a coluna inteira na memória.

É também uma boa ideia chamar ReadAsync : no modo não-seqüencial isso vai ler todas os dados da coluna, o que pode abranger vários pacotes, permitindo o acesso mais rápido aos valores da coluna.

A seguir uma breve descrição dos métodos usados:

  1. OpenASync() - Esta é a versão assíncrona do método Open. Os provedores devem ser sobrescritos com uma implementação adequada. A implementação padrão invoca a chamada Open síncrona e retorna uma tarefa concluída. Exceções geradas por Open serão comunicadas através da propriedade Exception Task retornada. Não invoque outros métodos e propriedades do objeto DbConnection até que a tarefa retornada esteja completa.
  2. ExecuteReaderAsync() - Inicia a execução assíncrona da instrução Transact-SQL ou procedimento armazenado que é descrito por este SqlCommand.
  3. ReadAsync() - Uma versão assíncrona de método Read, que avança o leitor para o próximo registro em um conjunto de resultados. Este método chama ReadAsync com CancellationToken.None.
  4. GetFieldValueAsync<T>() - Obtém o valor especifica pela coluna do tipo de forma assíncrono.

Pegue o projeto completo aqui: ProgramacaoAssincrona.zip

João 7:37 Ora, no seu último dia, o grande dia da festa, Jesus pôs-se em pé e clamou, dizendo: Se alguém tem sede, venha a mim e beba.

João 7:38 Quem crê em mim, como diz a Escritura, do seu interior correrão rios de água viva.

Referências:


José Carlos Macoratti