Por debaixo do capô: async/await e as mágicas do compilador csharp
Published: 2023-10-18 22:38:15

No [post anterior](https://dev.to/angelobelchior/por-debaixo-do-capo-c-e-acucar-sintatico-3gji) 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"](https://www.sympla.com.br/evento-online/apis-robustas-com-asp-net-core-8-entity-framework-docker-e-redis/2315443?referrer=www.vemcodar.com.br) 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/](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_](https://learn.microsoft.com/pt-br/dotnet/api/system.net.http.httpclient?view=net-7.0)**.

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/](https://learn.microsoft.com/pt-br/dotnet/csharp/asynchronous-programming/)

Abaixo segue nossa classe.

```csharp
// 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](https://dev.to/angelobelchior/por-debaixo-do-capo-c-e-acucar-sintatico-3gji) 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:

```csharp
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/](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ô:


```csharp
// 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);
    }
}
```
![Jackie Chan e Cris Rock no filme "A hora do Rush" numa cena dentro de um taxy fazendo cara de pânico](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p6tp8n05koe7v90hyewh.jpg)

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](https://plugins.jetbrains.com/plugin/12024-cognitivecomplexity) que me dá essa informação:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rasjw13vbbiuzjzy55nm.png)

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](https://github.com/dotnet/roslyn) 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á](https://www.youtube.com/shorts/huBEoFijyU8), **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**](https://pt.wikipedia.org/wiki/M%C3%A1quina_de_estados_finita).

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:

```csharp
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 <u>Inicial</u>: 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.

```csharp
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...

```csharp
awaiter = _httpClient.GetAsync(requestUri).GetAwaiter();
```
... e com isso, damos início ao segundo estado...

**Estado <u>Em Execução</u>: State = 0:** 

```csharp
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](https://learn.microsoft.com/pt-br/dotnet/csharp/language-reference/keywords/ref).

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:

```csharp
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 <u>Obtendo Resultado</u>: 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`.

```csharp
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**).

```csharp
...
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 <u>Ocorreu um Erro</u>: 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.

```csharp
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](https://dev.to/angelobelchior/por-debaixo-do-capo-c-e-acucar-sintatico-3gji) deve ter notado que a instrução `using` se torna um `try/finally`, e é no `finally` que os objetos são "descartados".

```csharp
...

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`:

```csharp
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 o `builder` da máquina de estados gera de retorno para o método `ObterPostPorIdAsync`: `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](https://learn.microsoft.com/pt-br/dotnet/api/system.threading.tasks.task?view=net-7.0)** 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](https://github.com/angelobelchior/AsyncAwaitSample)!
**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/](https://devblogs.microsoft.com/pfxteam/asyncawait-faq/). 
E a melhor parte está nos comentários. Leitura obrigatória.

Era isso, até a próxima!