Programação Paralela – Parte 4 – I/O Assícrono com Async/Await

Introdução

Na parte 3 desta série, vimos como o I/O assíncrono pode ser mais eficiente, liberando a CPU da thread que espera por I/O para fazer outra atividade, aumentando o desempenho geral da aplicação.

Vimos também como essa implementação pode ser complexa. O objetivo deste post é mostrar como as novas features de async/await, implementadas no framework 4.5 podem simplificar este processo.

Utilizando o async/await

No exemplo anterior, utilizamos um método que faz uma grande escrita em disco, seguida de um grande processamento. Vamos recriar este exemplo, utilizando o padrão async/await.

        [TestMethod]
        public void BigAsyncIOTest2()
        {
            Stopwatch watch = new Stopwatch();
            watch.Start();
            
            for (int i = 0; i < 10; i++)            
                DoBigWriteAsync().Wait();
            
            watch.Stop();
            Console.WriteLine("Total elapsed (ms): " + watch.ElapsedMilliseconds);
        }

        private async Task DoBigWriteAsync()
        {
            FileStream fs = new FileStream("file1.txt", FileMode.Create, FileAccess.ReadWrite);
            byte[] buffer = new byte[100 * 1024 * 1024]; //Não faça isso. Aloca 100Mb de memória.                        
            Task t = fs.WriteAsync(buffer, 0, buffer.Length);           
            Task t2 = t.ContinueWith(new Action((task) => { 
                fs.Flush();
                fs.Close();
            }));
            BubbleSort.DoBigBubbleSort(10000, null);
            await t;            
        }

O método DoBigWriteAsync teve um modificador “async” iniciado e um “await t” inserido no final. O método WriteAsync implementado pela classe Stream, provê os mesmos mecanismos. Na verdade em nível mais baixo da API é ele quem faz o tratamento da chamada de I/O assícrono.

Na prática, o que acontece nesta implementação é que cada vez que WriteAsync é chamado, uma continuação é inserida (para dar o Flush e o Close na Stream) e o BubbleSort é iniciado. Somente quando ocorre o “await t”, é aguardado o término do I/O para que o método devolva o resultado.

Na prática, o tempo que o método ficou parado com “await” não travou a thread da aplicação e sim a disponibilizou para executar outras atividades enquanto o I/O está em andamento. Aí está toda a mágica e a simplicidade do async/await. Você escreve um método com a mesma característica e simplicidade que escreve um método sícrono, porém, o framework não travará a thread.

Qual o desempenho?

Pelos testes muito simples executados nesta série, podemos perceber que há um overhead em relação ao método assíncrono puro (utilizado na parte 2 da série). O método executou em 13835 milisegundos, contra 9111 milisegundos da implementação pura. Meu palpite é que este overhead ocorre devido à mecânica da Task Parallel Library que exploraremos em seguida.

Quais as vantagens de usar?

Praticamente todas as API’s de I/O no framework estão sendo reescritas para suportar este padrão, entre elas: Acesso HTTP, Sockets, Web Services, Banco de dados (DataReader), etc. A lista completa está em: http://msdn.microsoft.com/pt-br/library/hh191443.aspx.

Você pode fazer, com baixo esforço, sua aplicação sofrer muito menos com problemas de responsividade e espera por I/O. Somente no exemplo acima, partimos de 20 segundos numa implementação totalmente síncrona para 14 segundos, basicamente trocando keywords nos métodos.

Como funciona?

Sempre que um método assíncrono (Ex.: WriteAsync) é chamado, o mesmo inicia a operação de I/O e cria uma Task que somente conclui quando o retorno do callback avisando que a operação de I/O terminou. Ou seja, quando o callback termina, a task é marcada como concluída. Nenhuma nova thread (worker thread) é criada neste processo. Este é um dos pontos que mais me gera confusão, pois apesar da classe Task fazer parte do namespace System.Threading, não significa que sempre que uma task é criada a mesma é executada numa thread separada.

Pelo próprio Task Parallel Library, toda nova task, é criada numa TaskFactory padrão, que utiliza um TaskScheduler padrão. O TaskScheduler padrão do framework é um ThreadPoolTaskScheduler. Ele tem um comportamento que a cada nova Task ele delega para seu thread pool a sua execução. Em outras palavras, ele tenta utilizar alguma thread livre do seu thread pool. Se não possui nenhuma, cria uma. Se possui threads demais (maior que a quantidade de cores da máquina), ele aguarda alguma task finalizar e pega uma das threads já criadas.

Com este comportamento, ele consegue manter o maior paralelismo possível, com a menor quantidade de trocas de contexto possível. Esperto, né?

Esse comportamento também proporciona um outro comportamento desagradável que é o fato de não sabermos exatamente em que thread a Task será executada, o que pode trazer potenciais problemas de locks, deadlocks, ou problemas com código não thread-safe sendo executado dentro de uma task. É o preço.

Por isso o overhead imposto acima. Numa execução simples, com a máquina com pouco processamento (cenário deste teste), há um incremento do tempo devido ao overhead imposto pelo processo de escalonamento das tasks. Num cenário de alta carga, o task scheduler deve valer seu preço.

Em contrapartida, sempre que executamos uma operação de I/O assíncrono no modelo async/await, esta task é enfileirada no TaskScheduler. Se ela está aguardando conclusão, a thread que originalmente estaria esperando pega outra Task e a executa. Essa outra task pode ser outra operação de I/O, um pedaço de processamento paralelo utilizado numa expressão PLINQ (parallel LINQ) ou qualquer outro processamento do framework que esteja encapsulado numa task.

Era isso.

Anúncios

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s