BT

Disseminando conhecimento e inovação em desenvolvimento de software corporativo.

Contribuir

Tópicos

Escolha a região

Início Notícias .NET: Os custos de Async e Await

.NET: Os custos de Async e Await

Técnicas de programação assíncrona podem oferecer melhoras significativas no desempenho geral de uma aplicação, mas os ganhos não vêm sem custos. Uma função assíncrona é frequentemente mais lenta que sua alternativa síncrona, e se não for tomado o devido cuidado, haverá aumento do consumo de memória. Stephen Toub, da MSDN Magazine, escreveu sobre o tema em um artigo intitulado Desempenho Assíncrono: Compreendendo os custos do Async e Await (em inglês).

Uma das maiores vantagens do código gerenciado sobre código C++ nativo é a capacidade de fazer o inlining de chamadas de funções em tempo de execução. O compilador JIT do CLR pode até mesmo fazer inlining de funções em assemblies diferentes, reduzindo o custo adicional para métodos com menor granularidade (preferidos pelos desenvolvedores OO). 

Infelizmente, devido à própria natureza das chamadas assíncronas, funções delegadas (delegates) não pode sofrer inlining. Além disso, há bastante código repetitivo envolvido na preparação de uma chamada assíncrona. Isto leva à primeira sugestão de Stephen Toub: "Pense em blocos; não em diálogos". De forma similar à chamada de componentes COM ou p/invoke boundary, devem ser feitas poucas chamadas grandes e assíncronas, em vez de muitas chamadas pequenas.

Há vários casos em que o uso dos padrões assíncronos pode leva à alocação de memória sem o desenvolvedor explicitamente usar o operador new. Se não forem controladas, essas alocações podem levar ao consumo excessivo de memória e a esperas indesejadas pelo coletor de lixo. Considere a seguinte assinatura e a instrução de retorno de uma subclasse de Stream:

public override async Task<int> ReadAsync(…)
return this.Read(…)

No código não é mostrado que um objeto Task é criado implicitamente para encapsular o inteiro retornado por Read(). Stephen Toub mostra como reduzir a carga adicional através do cache do último objeto Task<int>, que depois é reutilizado.

Outra causa de alocação de objetos inesperada é o uso de closures. Em C# e VB, closures são implementados como classes anônimas, contendo as funções anônimas e assíncronas declaradas no método. As variáveis locais ​​necessárias para essas funções são ditas "envolvidas" (closed over) ou "elevadas" (lifted) para dentro da classe anônima. Uma instância da classe deve ser criada cada vez que o método pai é chamado.

Mas os problemas não param por aí. Normalmente os objetos referenciados por variáveis ​​locais são coletados antecipadamente: o GC pode recuperá-los tão logo fique claro que não serão utilizados novamente na função atual. Como as variáveis locais utilizadas por uma funcão assíncrona são, na verdade, atributos de uma classe anônima, devem ser mantidas pela duração total da chamada. 

Se isso demorar vários segundos, o que não é incomum para uma chamada assíncrona, a classe anônima pode ser promovida para a geração 1 ou 2, que são maiss custosas. Se isso se tornar um problema, Stephen Toub recomenda atribuir null às variáveis locais, tão logo deixem de ser necessárias.

A terceira questão discutida por Toub é o conceito de contextos, especificamente o contexto de sincronização e o contexto de execução. Seu artigo mostra como um código de biblioteca pode ganhar em desempenho se ignorar intencionalmente o contexto de sincronização, através do método ConfigureAwait, com isso evitando operações que requerem a captura do contexto de execução.

Avalie esse artigo

Relevância
Estilo/Redação

Conteúdo educacional

BT