segunda-feira, 28 de janeiro de 2013

Utilizando Delegates com Co-Rotinas - parte 1

No post anterior comentei sobre delegates, funcs e actions, mas infelizmente não dei exemplos práticos de utilização.

Nesta primeira parte do post, introduzirei o conceito de co-rotina e um exemplo de utilização.

Co-Rotinas


Co-rotinas facilitam a programação e visualização de código quando temos a necessidade de fazer chamadas assíncronas de forma sequencial, ou seja, esperar o resultado de uma operação antes de iniciar a próxima operação, permitindo que tomemos decisões de acordo com os resultados obtidos.

Na programação síncrona, o fluxo da execução de um programa ocorre em uma mesma Thread. Na programação assíncrona, uma outra Thread é criada para execução de uma determinada tarefa que tem um tempo de duração indefinido, como por exemplo, chamadas a serviços da Web.

Geralmente os métodos assíncronos possuem eventos ou callbacks que são disparados no momento em que a operação for finalizada/completada, permitindo que o programador escreva código para tratar o resultado.  A dor de cabeça vem quando a lógica de uma aplicação fica complexa, por exemplo, quando há a necessidade de fazer várias chamadas utilizando eventos e callbacks, tornando o código difícil de ler, e sujeito a bugs.

Para exemplificar, vamos escrever um programa simples que faz o download do código html de uma página, e imprime o conteúdo numa aplicação console:

     class Program
    {    
        public static void Main(string[] args)
        {
            var webClient1 = new WebClient();
            webClient1.DownloadStringCompleted += (s1, e1) => {
                Console.WriteLine(e1.Result);            
            };
            webClient1.DownloadStringAsync(new Uri("http://silverlightrush.blogspot.com"));    
            Console.ReadLine();
        }
    }


Ok, esse código quando rodado irá instanciar uma classe chamada WebClient. Esta classe contém um método chamado DownloadStringAsync. Quando executado, o código não "espera" o download terminar, pois é um método assíncrono, e então segue para a próxima linha. Antes de chamar o método, devemos subscrever ao evento DownloadStringCompleted. Através de uma expressão lambda, imprimimos o conteúdo da página toda na tela.

Agora vamos inserir uma regra de negócio ao programa. Digamos que ao terminar de ler o html, caso o Lenght do resultado seja maior que 1000 bytes, faremos uma outra chamada para baixar o html de um outro site. Então o novo código ficou definido assim:
     
    var webClient1 = new WebClient();
    webClient1.DownloadStringCompleted += (s1, e1) => 

    {
        if (e1.Result.Length < 1000)
            Console.WriteLine(e1.Result);
        else {
            var webClient2 = new WebClient();
            webClient2.DownloadStringCompleted += (s2, e2) => {
                Console.WriteLine(e2.Result);
            };
            webClient2
.DownloadStringAsync(new Uri("http://google.com"));
        };
    };
    webClient1
.DownloadStringAsync(new Uri("http://silverlightrush.blogspot.com"));
    Console.ReadLine();

Pronto. A nossa primeira regra de negócio foi inserida. O código funciona perfeitamente. Mas e se agora quisermos inserir uma nova regra baseado no resultado da segunda chamada ? Repare que o código está difícil de ler e provavelmente vai ficar difícil de dar manutenção.

Utilizando Co-Rotinas


Utilizando a técnica de co-rotinas, escrevemos código sequencial, enfileirando diversas chamadas assíncronas, utilizamos os tradicionais callbacks apenas dar prosseguimento ao fluxo de trabalho.


Vamos ver como fica a sintaxe para esse mesmo programa mas com a utilização de co-rotinas:

    class Program
    {
        static string htmlBlogspot;
        static string htmlGoogle;
    
        public static void Main(string[] args)
        {
            Coroutine.BeginExecute(Operacoes());        
            Console.ReadLine();
        }
    
        static IEnumerable<Routine> Operacoes() {
            yield return PrimeiraOperacao;
        
            if (htmlBlogspot.Length < 1000) {
                Console.WriteLine(htmlBlogspot);
                yield break//cancela a execução da próxima
            }
        
            yield return SegundaOperacao;
        
            Console.WriteLine(htmlGoogle);
        }
    
        static void PrimeiraOperacao(Action completed) {
            var webClient1 = new WebClient();
        
            webClient1.DownloadStringCompleted += (s1,e1) => {
                htmlBlogspot = e1.Result;
                completed(); //marca o fim da operação
            };
            webClient1.DownloadStringAsync(new
                Uri("http://silverlightrush.blogspot.com"));        
        }
    
        static void SegundaOperacao(Action completed) {
            var webClient2 = new WebClient();
            webClient2.DownloadStringCompleted += (s2, e2) => {
                htmlGoogle = e2.Result;
                completed(); //marca o fim da operação
            };
            webClient2.DownloadStringAsync(new Uri("http://google.com"));
        }
    }



O novo código:

  • A co-rotina é inicializada por Coroutine.BeginExecute(Operacoes()) 
  • A co-rotina é executada linha a linha dentro do método Operacoes()
  • O método Operacoes() retorna IEnumerable<Routine>
  • Cada operação assíncrona é executada no momento em que utilizamos yield return
  • A co-rotina pode ser cancelada com yield break;
  • Cada rotina ficou separada em seu próprio método. Quando a rotina completa, chama-se a action completed()

Como isso é possível ?


O Código abaixo é responsável pela enumeração e execução sequencial da co-rotina. Através da iteração do método que retorna IEnumerable<Routine>, o compilador C# faz uma mágica criando uma State Machine.




Delegates

Repare que foi criado um Delegate especial chamada Routine. Este delegate é utilizado no enumerador da co-rotina. A action que se passa como parâmetro é uma referência a um callBack que devemos executar para dar continuação à enumeração (MoveNext()).

Para ver o código completo no GitHub, clica no link abaixo.

Nenhum comentário:

Postar um comentário