Async não é paralelismo e não… Async não é mesma coisa que sync.- Parte 1

Intro

Calma, leia até o final :sweat_smile: juro que fará sentido o título, bom… pra mim fez…..

Não sei o quanto vocês conhecem de Async e a famosa maquina de estado no contexto de C#

particularmente eu já tive vários problemas e cheguei odiar porque sempre tive problema e sempre culpei o compilador….

bom, depois de criar vergonha na cara e meter a fuça em como as coisas funcionam aprendi um pouquinho, o suficiente pra não cair em algumas armadilhas.

tenho alguns Links de referência como meu book of reference, tudo que achei útil quando tive problemas eu acabei adicionando nele pra estudar aos poucos.

A importância de saber sobre async e sua máquina de estado.

Recentemente fui convidado pra fazer um Code Review em um código, particularmente devido o projeto que toco é complicado fazer code-review e quando acontece é algo bem pontual onde preciso separar na agenda.

e encontrei alguns itens que me chamaram atenção, foi removido paralelismo, remoção de async e foi adicionado um tal de GC.Collect().

Não me entenda mal, mas se você faz isso, talvez seja importante ler o livro: Pro .Net Memory Management: For Better Code, Performance, and Scalability by Kokosa ele dá um background ótimo sobre com funciona GC e convenhamos… A linguagem já tem mais de 20 anos, com engenheiros super competentes que pensaram durante anos da forma mais eficiênte deste processo, vamos ter humildade :sweat_smile:, existem pouquíssimos casos que você realmente precisa fazer isso, pelo que me recordo em debugging ou fazendo benchmark, com excessão desses pontos, você vai apenas piorar a heurística e se ele está sendo o problema… novamente, leia o livro do Kokosa e comece usar o Stack.

bom pulando pra parte que eu queria falar :sleeping:

Async at last!

não sei se vocês tem o costume de ler o que o código vai gerar ao escrever o código, não é sempre que você precisa disso, na verdade você ignora essa parte porque são poucos cenários que você precisará entender o que acontece por trás do código gerado ou IL pra determinar uma nova forma de escrever código

um código simples :

using System;
using System.Threading.Tasks;
public class TestSubject {
    public void DoSomething() {
    }
    public async Task DoSomethingAsync() {
    }
}

e o compilador irá traduzir isso para:

using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security;
using System.Security.Permissions;
using System.Threading.Tasks;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.0.0.0")]
[module: UnverifiableCode]
public class TestSubject
{
    [CompilerGenerated]
    private sealed class <DoSomethingAsync>d__1 : IAsyncStateMachine
    {
        public int <>1__state;

        public AsyncTaskMethodBuilder <>t__builder;

        public TestSubject <>4__this;

        private void MoveNext()
        {
            int num = <>1__state;
            try
            {
            }
            catch (Exception exception)
            {
                <>1__state = -2;
                <>t__builder.SetException(exception);
                return;
            }
            <>1__state = -2;
            <>t__builder.SetResult();
        }

        void IAsyncStateMachine.MoveNext()
        {
            //ILSpy generated this explicit interface implementation from .override directive in MoveNext
            this.MoveNext();
        }

        [DebuggerHidden]
        private void SetStateMachine(IAsyncStateMachine stateMachine)
        {
        }

        void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
        {
            //ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
            this.SetStateMachine(stateMachine);
        }
    }

    public void DoSomething()
    {
    }

    [AsyncStateMachine(typeof(<DoSomethingAsync>d__1))]
    [DebuggerStepThrough]
    public Task DoSomethingAsync()
    {
        <DoSomethingAsync>d__1 stateMachine = new <DoSomethingAsync>d__1();
        stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();
        stateMachine.<>4__this = this;
        stateMachine.<>1__state = -1;
        stateMachine.<>t__builder.Start(ref stateMachine);
        return stateMachine.<>t__builder.Task;
    }
}

é possível perceber que toda vez que você deixa uma assinatura de “async” no método ele gera uma máquina de estado, é até um dos motivos que eu tento não criar nested async, afinal invariávelmente será alocado em memória, então em cenários críticos eu penso bem… Continuando….

Precisava dar esse contexto caso alguém não tivesse ainda a oportunidade de saber o que acontece por baixo dos panos, então foi um resumo pra continuar

em minha memória do código de algum tempo atrás era o seguinte:

public async Task SomeMainTask(){

    var id = 0;
    var processoUm = Task.Run(()=>{ id = DoSomething();});
    var processoDois = DoOtherThing();

    await processoUm;

    (... outros processos sincronos e assíncronos)

    var processoDois = DoOtherThing(id);

    Task.WaitAll(
        processoDois,
        (...)
    );
}

public async Task<id> DoSomething(){
    (... alguma chamada pro banco)
    ///código apenas pra contexto
    return Task.FromResult(id);

}

e quando eu li o código:

public void  SomeMainTask(){

    var id = 0;
    var processoUm = DoSomething();
    var processoDois = DoOtherThing();

    (... outros processos sincronos e assíncronos)

    var processoDois = DoOtherThing(id);    
}

public id DoSomething(){
    (... alguma chamada pro banco)
    ///código apenas pra contexto
    return id;

}

eu fiquei meio intrigado, eu queria entender a motivação… particularmente eu faço tudo sincrono mesmo e eventualmente quando estou fazendo meu próprio code-review eu penso se faz ou não sentido colocar um async.

quando questionei, a resposta foi que async e sincrono seriam as mesmas coisas no final das contas ou seja :

isso

var id = DoSomething();

public int DoSomething(){
    (...chamada pro banco)
    return someid;
}

é igual isso:

var id = await DoSomething();

public Task<int> DoSomething(){
    (...chamada pro banco)
    using(var someconn = new SqlConnection(connectionstring)){
        return someconn.FirstOrDefaultAsync<int>("select 1");   
    }   
}

será?

tentei explicar que não era… afinal na própria documentação da microsoft (inclusive… que maravilha de doc ) explica exatamente o que eu disse. Apesar do meu esforço de mostrar que ele vai bloquear uma thread até ter resposta…

achei mais simples criar um código muito simples pra apresentar o problema:

Apenas pra dar contexto, eu sou um usuário assíduo do LinqPad, logo quando estiver escrito “someText”.Dump(); no código abaixo, entenda como Console.WriteLine(“sometext”);

async System.Threading.Tasks.Task Main()
{
    int nWorkers; 
    int nCompletions; 

    ///aqui estou forçando que apenas exista uma única Thread no meu sistema pra apresentar um cenário que dá pra observar o comportamento
    ThreadPool.SetMinThreads(1, 1).Dump();
    ThreadPool.SetMaxThreads(1, 1).Dump();

    /// apenas me assegurando que é apenas uma thread..... vai que... 
    ThreadPool.GetMaxThreads(out nWorkers, out nCompletions);
        "workers: {nWorkers}".Dump();"nCompletions: {nWorkers}".Dump();

    ///estou deixando uma thread executando sem parar, com um intervalo de 2 segundos, como se fosse um heart-beat.
    System.Threading.Tasks.Task.Run(async ()=>{
                while(true){
                    "Heartbeat".Dump();
                     await System.Threading.Tasks.Task.Delay(2000);
                }
    });


    for(var i = 0; i<=4; i++){
        /// aqui estou criando algumas Tasks assíncronas para executar alguma ação representando um insert no banco.
        await System.Threading.Tasks.Task.Run(async ()=>{
                await System.Threading.Tasks.Task.Delay(5000);"{Thread.CurrentThread.ManagedThreadId}-{DateTime.Now.ToString("HH:MM:ss")}".Dump();
        });
    }

    "-----------------------Async Done-----------------------".Dump();
    for(var i = 0; i<=4; i++){
        /// aqui estou criando as mesmas Tasks mas desta vez estou forçando a execução síncrona para executar a mesma ação no banco.
            System.Threading.Tasks.Task.Run(()=>{
                System.Threading.Tasks.Task.Delay(5000).Wait();
                $"{i} - {Thread.CurrentThread.ManagedThreadId}-{DateTime.Now.ToString("HH:MM:ss")}".Dump();
            });
    }
}

O que você acha que aconteceu?

eu tive que gravar um vídeo…:sweat_smile:

como esperado a thread que está resposável pelo heart-beat continua a execução, é liberada e então a task Async de “persistência no banco” consegue trabalhar, termina e assim o fluxo continua..

agora quando chega no síncrono… nenhum funciona, infelizmente não consegui compreender o motivo, a thread acabou ficando em lock no Task.Delay, mas isso eu não vou tentar entender hoje.

mas espero que esse post traga a luz para alguém que assim como eu também já sofreu com async.

p.s. eu sei que acontecer um thread starvation em alguns cenários é difícil dependendo do seu cenário, mas no cenário que eu verifiquei isso pode ser a diferênça entre aumentar a capacidade do servidor pra dar vazão no processamento, sendo que o custo pode ser evitado com um uso inteligênte dos recursos.

p.s² Pegadinha! eu não falei sobre paralelismo ainda :laughing:, mas o que eu falaria não escapa da gloriosa doc da mãecrosoft:

e depois continuo :p

Esteja Curioso!

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *