No post anterior eu falei um pouco sobre Açúcar Sintático e como o compilador do csharp trabalha para facilitar nossas vidas.
Antes de continuar, #VemCodar com a gente!!
Tá afim de criar APIs robustas com .NET?
Feito de dev para dev, direto das trincheiras, por quem coloca software em produção há mais de 20 anos, o curso "APIs robustas com ASP.NET Core 8, Entity Framework, Docker e Redis" traz de maneira direta, sem enrolação, tudo que você precisa saber para construir suas APIs de tal forma que elas sejam seguras, performáticas, escaláveis e, acima de tudo, dentro de um ambiente de desenvolvimento totalmente configurado utilizando Docker e Docker Compose.
Não fique de fora! Dê um Up na sua carreira!
O treinamento acontecerá nos dias 27/02, 28/02 e 29/02, online, das 19.30hs às 22.30hs
Acesse: https://vemcodar.com.br/
Propositalmente eu deixei de fora a feature async/await, queria fazer algo mais elaborado para explicar como as coisas funcionam por debaixo do capô quando utilizamos métodos assíncronos.
O cenário que eu trago aqui é simples e muito comum: Uma classe BlogService
que contém um método chamado ObterPostPorIdAsync
no qual faz uma requisição assíncrona a uma API, utilizando o HttpClient.
Eu escolhi justamente esse cenário porque o método que faz a requisição (GetAsync
) e o método que obtém o conteúdo de resposta como string (ReadAsStringAsync
) são assíncronos.
Nesse ponto eu assumo que você conheça o mínimo sobre async/await, isso é fundamental! Caso contrário, recomendo fortemente estudar o assunto. Esse link da Microsfoft vai te ajudar: https://learn.microsoft.com/pt-br/dotnet/csharp/asynchronous-programming/
Abaixo segue nossa classe.
// Escrito por mim
public class BlogService
{
public async Task<Post?> ObterPostPorIdAsync(int postId)
{
var endpoint = $"https://jsonplaceholder.typicode.com/posts/{postId}";
using var httpClient = new HttpClient();
using var response = await httpClient.GetAsync(endpoint);
var json = await response.Content.ReadAsStringAsync();
var post = JsonSerializer.Deserialize<Post>(json);
return post;
}
}
Se você já tem um certo conhecimento sobre csharp, consumo de apis etc., não deve ter nenhuma dúvida sobre como esse código funciona.
Porém, em resumo, temos:
- Um cliente
Http
é criado; - Fazemos uma requisição ao endpoint;
- Obtemos a resposta dessa requisição e extraímos seu conteúdo como string (nesse caso, essa string vem no formato Json) ;
-
Desserializamos esse conteúdo como um objeto
Post
.
Reforço aqui que meu objetivo é ser didático, por isso não temos validações do response, políticas de retentativas e etc.
Se você leu atentamente o post anterior notou que o compilador do csharp ao gerar o código IL acaba por adicionar caracteres especiais em nome de métodos, classes e etc, algo como:
private struct <Main>d__0
{
//...
}
Claramente esse código não compila! Porém, ao obter o código gerado pelo compilador através do site https://sharplab.io/ resolvi ajustá-lo para que fosse possível sua execução.
Respira fundo! Vai com calma e não se preocupe! Eu vou explicar direitinho o que acontece por debaixo do capô:
// Gerado pelo compilador, ajustado por mim
public class BlogService
{
private struct ObterPostPorIdAsyncStateMachine : IAsyncStateMachine
{
public int State;
public AsyncTaskMethodBuilder<Post> Builder;
public int PostId;
private HttpClient _httpClient;
private HttpResponseMessage _httpResponseMessage;
private TaskAwaiter<HttpResponseMessage> _awaiterHttpResponseMessage;
private TaskAwaiter<string> _awaiterContentString;
private void MoveNext()
{
var num = State;
Post result;
try
{
var requestUri = default(string);
if (num < 0)
{
var defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(43, 1);
defaultInterpolatedStringHandler.AppendLiteral("https://jsonplaceholder.typicode.com/posts/");
defaultInterpolatedStringHandler.AppendFormatted(PostId);
requestUri = defaultInterpolatedStringHandler.ToStringAndClear();
_httpClient = new HttpClient();
}
try
{
TaskAwaiter<HttpResponseMessage> awaiter;
if (num != 0)
{
if (num == 1)
{
goto IL_00b6;
}
awaiter = _httpClient.GetAsync(requestUri).GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (State = 0);
_awaiterHttpResponseMessage = awaiter;
Builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
return;
}
}
else
{
awaiter = _awaiterHttpResponseMessage;
_awaiterHttpResponseMessage = default(TaskAwaiter<HttpResponseMessage>);
num = (State = -1);
}
var httpResponseMessage = (_httpResponseMessage = awaiter.GetResult());
goto IL_00b6;
IL_00b6:
try
{
TaskAwaiter<string> awaiter2;
if (num != 1)
{
awaiter2 = _httpResponseMessage.Content.ReadAsStringAsync().GetAwaiter();
if (!awaiter2.IsCompleted)
{
num = (State = 1);
_awaiterContentString = awaiter2;
Builder.AwaitUnsafeOnCompleted(ref awaiter2, ref this);
return;
}
}
else
{
awaiter2 = _awaiterContentString;
_awaiterContentString = default(TaskAwaiter<string>);
num = (State = -1); //Reinicia a máquina de estados...
}
result = JsonSerializer.Deserialize<Post>(awaiter2.GetResult());
}
finally
{
if (num < 0 && _httpResponseMessage != null)
{
((IDisposable)_httpResponseMessage).Dispose();
}
}
}
finally
{
if (num < 0 && _httpClient != null)
{
((IDisposable)_httpClient).Dispose();
}
}
}
catch (Exception exception)
{
State = -2;
_httpClient = null;
_httpResponseMessage = null;
Builder.SetException(exception);
return;
}
State = -2;
_httpClient = null;
_httpResponseMessage = null;
Builder.SetResult(result);
}
void IAsyncStateMachine.MoveNext()
{
//ILSpy generated this explicit interface implementation from .override directive in MoveNext
this.MoveNext();
}
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
Builder.SetStateMachine(stateMachine);
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
//ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
this.SetStateMachine(stateMachine);
}
}
public Task<Post> ObterPostPorIdAsync(int postId)
{
var stateMachine = default(ObterPostPorIdAsyncStateMachine);
stateMachine.Builder = AsyncTaskMethodBuilder<Post>.Create();
stateMachine.PostId = postId;
stateMachine.State = -1;
stateMachine.Builder.Start(ref stateMachine);
return stateMachine.Builder.Task;
}
}
public record Post(
[property: JsonPropertyName("id")] int Id,
[property: JsonPropertyName("title")] string Title,
[property: JsonPropertyName("body")] string Body);
public static class Program
{
public static async Task Main()
{
var blog = new BlogService();
var post = await blog.ObterPostPorIdAsync(1);
Console.WriteLine(post);
}
}
Sem pânico!
Antes de tudo, vamos nos concentrar apenas na classe BlogService
! A classe Program
e o record Post
servem só de apoio!
Numa primeira análise notamos o quanto de código o compilador gerou e fica nítido que o foco é que ele seja performático e gere a menor quantidade de alocações possível.
Aliás, a complexidade cognitiva do método MoveNext
da struct ObterPostPorIdAsyncStateMachine
é de 18, sendo que o aceitável é 10.
Eu uso um plugin no Rider que me dá essa informação:
Mas faço questão de reforçar mais uma vez que esse é o código mais otimizado possível para essa situação. O time de desenvolvimento do compilador do csharp trabalha árduamente para que a cada versão sejam incluídas mais e mais otimizações em todo o processo de compilação!
Porém, diferente do Baianinho de Mauá, as mágicas que o compilador faz NÃO SÃO INFINITAS. Se seu código não for minimante bem escrito, não vai adiantar nada, por isso é importante entendermos o que se passa por debaixo do capô!.
Vamos ao que interessa!
A primeira coisa que precisamos entender é que o processo assíncrono é gerenciado por uma máquina de estados.
Dentro da classe BlogService
é criada uma struct privada chamada ObterPostPorIdAsyncStateMachine
. É nessa struct que toda mágica acontece. Cada método assíncrono existente na classe BlogService
teria sua própria máquina de estados. Nesse caso criei apenas um método para fins didáticos.
Essa struct implementa a interface IAsyncStateMachine
que fica dentro do namespace System.Runtime.CompilerServices e contém os seguintes métodos:
public interface IAsyncStateMachine
{
void MoveNext();
void SetStateMachine(IAsyncStateMachine stateMachine);
}
Essa interface representa uma máquina de estados geradas para métodos assíncronos e é destinada apenas ao uso do compilador.
O método MoveNext()
move a máquina de estados para o próximo estado.
Já o método SetStateMachine(IAsyncStateMachine stateMachine)
seta a máquina de estados com uma réplica alocada na memória heap.
Essa máquina tem 4 estados que são armazenados na variável global do tipo int
chamada State
. Note que dentro do método MoveNext
o valor da variável State
é copiado para a variável num
. Alterar diretamente o valor de uma variável global numa máquina de estados pode ser perigoso e trazer efeitos colaterais indesejados. Essa variável global só vai receber um valor quando seu estado, de fato, mudar.
Estado Inicial: State = -1:
Ao invocar o método MoveNext
pela primeira vez, a máquina de estados é iniciada. É nesse momento onde as variáveis são criadas.
if (num < 0)
{
var defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(43, 1);
defaultInterpolatedStringHandler.AppendLiteral("https://jsonplaceholder.typicode.com/posts/");
defaultInterpolatedStringHandler.AppendFormatted(PostId);
requestUri = defaultInterpolatedStringHandler.ToStringAndClear();
_httpClient = new HttpClient();
}
Após a criação das variáveis, é solicitada a requisição. Note que eu usei a palavra solicitada e não efetuada, afinal, como é mostrado no código, a variável awaiter
está aguardando o processamento...
awaiter = _httpClient.GetAsync(requestUri).GetAwaiter();
... e com isso, damos início ao segundo estado...
Estado Em Execução: State = 0:
if (!awaiter.IsCompleted)
{
num = (State = 0);
_awaiterHttpResponseMessage = awaiter;
Builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
return;
}
A variável awaiter
tem uma propriedade chamada IsCompleted
que indica se o processo está completo ou não (true/false
).
Como estamos na primeira rodada do processamento (o método MoveNext
foi acionado apenas uma vez), esse IsCompleted
é falso, fazendo com que o estado (State
) fique com o valor 0 e que seja invocado o método Builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
.
Esse é o ponto chave de todo o fluxo!
Esse método recebe o objeto que está aguardando a finalização de um processo (awaiter
) e qual objeto que o invocou (this
), sendo esse um tipo que implemente a interface IAsyncStateMachine
. Tudo via referência.
Basicamente o AwaitUnsafeOnCompleted
vai esperar por alguma mudança de estado no processamento do método aguardado pelo awaiter
(_httpClient.GetAsync(requestUri)
) e em seguida invoca o método MoveNext
da struct que o invocou (ObterPostPorIdAsyncStateMachine
).
Temos aqui um looping: Enquanto o awaiter.IsCompleted
não for true
, State
vai ficar como 0 e o método Builder.AwaitUnsafeOnCompleted
vai ser invocado novamente.
Esse processo todo também vai ocorrer quando estamos lendo o conteúdo do response após a requisição ser efetuada com sucesso:
awaiter2 = _httpResponseMessage.Content.ReadAsStringAsync().GetAwaiter();
if (!awaiter2.IsCompleted)
{
num = (State = 1);
_awaiterContentString = awaiter2;
Builder.AwaitUnsafeOnCompleted(ref awaiter2, ref this);
return;
}
Perceba que o fluxo é exatamente o mesmo!
Após os awaiters derem o sinal de que foram executados (IsCompleted == true
) passamos para o próximo estado da nossa máquina.
Estado Obtendo Resultado: State = 1:
Aqui chegamos ao final do nosso processo.
Com o json obtido através do response vamos desserializa-lo e criar um objeto Post
.
if (num != 1)
{
// *solitita* o conteúdo do response...
}
else
{
awaiter2 = _awaiterContentString;
_awaiterContentString = default(TaskAwaiter<string>);
num = (State = -1); //Reinicia a máquina de estados...
}
result = JsonSerializer.Deserialize<Post>(awaiter2.GetResult());
É possível notar o uso do go to (que me lembra o saudoso e famigerado VB6: On Error go to Hell).
...
if (num == 1)
{
goto IL_00b6;
}
...
IL_00b6: // <--------
try
{
TaskAwaiter<string> awaiter2;
if (num != 1)
{
awaiter2 = _httpResponseMessage.Content.ReadAsStringAsync().GetAwaiter();
if (!awaiter2.IsCompleted)
{
num = (State = 1);
_awaiterContentString = awaiter2;
Builder.AwaitUnsafeOnCompleted(ref awaiter2, ref this);
return;
}
}
else
{
awaiter2 = _awaiterContentString;
_awaiterContentString = default(TaskAwaiter<string>);
num = (State = -1);
}
result = JsonSerializer.Deserialize<Post>(awaiter2.GetResult());
}
finally
{
if (num < 0 && _httpResponseMessage != null)
{
((IDisposable)_httpResponseMessage).Dispose();
}
}
O compilador utiliza o go to de maneira estratégica, fazendo com que o código desvie para o trecho onde o segundo awaiter está: awaiter2 = _httpResponseMessage.Content.ReadAsStringAsync().GetAwaiter();
. Imagine que cada método async dentro desse processo poderia ter o seu go to
justamente para que o código consiga alcançá-lo a partir do momento que o método MoveNext
fosse executado. Normalmente usaríamos um if
ou quebraríamos o método em várias partes, mas como disse, o compilador escolhe a melhor maneira para ele e não para quem está lendo o código.
E se por acaso explodir um erro?
Estado Ocorreu um Erro: State = -2:
Todo processo é envolvido por um try/catch
e caso ocorra um erro, o State
é mudado para -2, as variáveis são setadas como nulo e o builder
armazena a exception gerada.
catch (Exception exception)
{
State = -2;
_httpClient = null;
_httpResponseMessage = null;
Builder.SetException(exception);
return;
}
Ainda temos trechos de código que são utilizados para darem dispose nos objetos. Se você acompanhou o post anterior deve ter notado que a instrução using
se torna um try/finally
, e é no finally
que os objetos são "descartados".
...
finally
{
if (num < 0 && _httpResponseMessage != null)
{
((IDisposable)_httpResponseMessage).Dispose();
}
}
...
finally
{
if (num < 0 && _httpClient != null)
{
((IDisposable)_httpClient).Dispose();
}
}
Por fim, e não menos importante temos o método: ObterPostPorIdAsync
:
public Task<Post> ObterPostPorIdAsync(int postId)
{
var stateMachine = default(ObterPostPorIdAsyncStateMachine);
stateMachine.Builder = AsyncTaskMethodBuilder<Post>.Create();
stateMachine.PostId = postId;
stateMachine.State = -1;
stateMachine.Builder.Start(ref stateMachine);
return stateMachine.Builder.Task;
}
Esse trecho não tem segredo! É nele onde os valores iniciais são setados e a máquina de estados é criada e inicializada.
Se você leu com atenção deve ter notado duas coisas:
- O
async
não existe! Essa keyword simplesmente some no código gerado. - A única
Task
que temos é a que obuilder
da máquina de estados gera de retorno para o métodoObterPostPorIdAsync
:return stateMachine.Builder.Task;
.
E é aqui aonde quero chegar: eu quis com esse post mostrar como o compilador lida com o async/await, como que o código é gerado e qual é a estratégia que ele usa para saber quando um processo terminou ou lançou um erro.
A classe Task por si só mereceria um post todinho só para ela.
Fora que ainda existem outros cenários que eu não cobri aqui, mas cobrirei em breve, como por exemplo o uso maléfico, maligno, molecular e abominável do .Result, CancellationToken em métodos assíncronos, a função do .ConfigureAwait(true/false) e o tratamento de exceções.
Quero escrever um post para cada um desses itens dando a devida atenção.
Para entender melhor todo o fluxo disponibilizei no github o código fonte!
Coloque um break point dentro do método MoveNext e vá navegando linha a linha.
Quer ir mais afundo nesse assunto?
Esse é, sem dúvidas, um dos melhores posts sobre async/await: https://devblogs.microsoft.com/pfxteam/asyncawait-faq/.
E a melhor parte está nos comentários. Leitura obrigatória.
Era isso, até a próxima!
Top comments (2)
Excelente post!
É sempre interessante saber como as coisas funcionam por baixo dos panos.
Muito obrigado! Quero fazer mais posts desse tipo \o/