Pular para o conteúdo

Racional · ensaio 03

Async e IO

rationale.md · 75 linhas · 3 min de leitura

A redefinição: async é colorless, e quem suspende é o processo, não a função. Não existe async/await/Future.

Concorrência de IO exige que algo “espere sem travar tudo”. As linguagens mainstream resolvem isso colorindo funções: async fn em Rust/JS/Python marca uma função como suspensível, e essa marca propaga: quem chama uma async precisa ser async (ou await), até o topo. Nasce o “function coloring”: metade da linguagem fica de uma cor, metade de outra, e as duas não se misturam.

Não há cor. Nenhuma função é async. O bloqueio em IO é transparente na sintaxe: você escreve data := -> chan ou fs.read(...) como se fosse síncrono, e o processo suspende ali. O scheduler pega outro processo enquanto este espera. Os pontos de suspensão são explícitos e poucos (operações de io.*, send/recv de channel, o <-> síncrono), mas nenhum deles aparece como marca de tipo.

A consequência direta: paralelismo é spawn de processos, não composição de Futures. Para fazer N coisas ao mesmo tempo, você spawna N processos e recebe os resultados por channel (cap. 04). Não há Future/await/executor para gerenciar.

A pergunta de fundo é: a assinatura de uma função deve dizer como ela roda? A posição de Makoto é não. Uma assinatura diz o que a função faz, nunca como ela é escalonada. “Roda de forma suspensível” é detalhe de implementação, e detalhe de implementação não deve colorir o tipo. Essa é a distinção que destrava tudo: coloring de contrato vs coloring de implementação. unsafe é coloring de contrato (informa um perigo real que você precisa conhecer), e é aceitável. async é coloring de implementação (vaza um detalhe de execução para o tipo), e é recusado. A diferença operacional sela o veredito: async é incontível (a cor sobe infinitamente até o main); unsafe é contível (qualquer camada pode absorver o perigo e parar a propagação). Uma cor que não se contém é exatamente o tipo de mancha que a filosofia (cap. 01) existe para impedir.

Isso encaixa no modelo de memória e de concorrência sem costura: como o processo é a unidade de execução com uma única linha (cap. 02/04), “suspender o processo” é uma operação natural, e não precisa de uma máquina de estados de corotina exposta ao programador. O runtime faz o trabalho; a linguagem fica colorless.

  • async/await/Future (Rust/JS/Python). Coloring de implementação: infecta o sistema de tipos, força composição via blocos async, e adiciona Future como mais um tipo de primeira classe a gerenciar. Ganho real sobre “o processo suspende invisível”: nenhum.
  • Callbacks / promises. Inversão de controle, “callback hell”, máquinas de estado manuais. Pior ergonomia para o mesmo efeito.
  • Threads de SO por requisição. Caras demais para o grão de concorrência que a BEAM-like quer (milhões de processos); e reabririam memória compartilhada (cap. 04 explica por que não).

O “what color is your function” clássico: você tem uma função síncrona perfeitamente boa e descobre que precisa chamar algo async lá dentro. Agora ela tem que ser async, e quem a chama também, e assim por diante, até você reescrever a árvore inteira ou manter duas versões de tudo. A cor que não se contém custa retrabalho real, e não compra correção nenhuma que o modelo de processo não dê de graça.

A unidade de suspensão é o processo. Você escreve IO como se fosse síncrono; o processo para, o scheduler segue. Bloqueio é fato, não marca de tipo.

Colorless não significa “tudo implícito”. O <-> (request-reply síncrono) exige timeout explícito, uma marca obrigatória, ao contrário do <- silencioso. É uma assimetria deliberada: deadlock entre dois processos que se esperam é estrutural, e o prazo é a saída honesta (cap. 04). E shared-nothing tem custo de cópia/passagem de mensagem; Makoto aceita correção e distribuição-nativa sobre throughput bruto de memória compartilhada.

Próximo: 04 · Concorrência