Pular para o conteúdo

Racional · ensaio 04

Concorrência

rationale.md · 83 linhas · 3 min de leitura

A redefinição: shared-nothing, com atores isolados conversando por channels tipados, sob supervisão. O mesmo Channel[T] serve local e rede.

Concorrência mainstream é threads + memória compartilhada + locks. Você cria threads, elas compartilham o heap, e mutexes/atomics protegem o que é compartilhado. Funciona, mas a categoria de bug “esqueci de sincronizar” é endêmica, e o modelo não atravessa a rede (mutex não funciona entre máquinas).

A herança é a da BEAM/Erlang: a unidade é o processo, isolado, com heap próprio, barato. Processos não compartilham memória (não existe @shared). Eles conversam por channel tipado, Channel[<-T, ->U], onde a notação é a perspectiva de quem segura o handle: <-T = “eu envio T”, ->T = “eu recebo T” (elimina a inversão de tipos na assinatura do outro lado). Operadores: chan <- data (send), data := -> chan (receive), e <-> (request-reply síncrono).

E a virada que unifica dois mundos: o channel de rede é o mesmo Channel[T] do local. Não há “channel local” vs “RPC remoto”; há um tipo, dois contextos. A física da rede só se expressa por duas exigências que já existem na linguagem: o payload precisa virar bytes (T + Serializable) e estabelecer pode falhar (Result + timeout). Isso é o RPC tipado nativo: os dois lados compartilham T em compilação, sem IDL nem stub gerado.

Por que shared-nothing? Porque é o que torna o resto do design possível. Sem aliasing concorrente, o borrow checker fica desnecessário (cap. 02); a memória pode ser colorless; um panic num processo não corrompe o heap de outro. O isolamento não é uma restrição imposta; é a fundação de onde a segurança de memória sem lifetimes e a tolerância a falhas brotam.

Por que o <-> exige timeout? Porque dois processos fazendo <-> um no outro dariam deadlock garantido, e um deadlock silencioso é a pior falha possível: o sistema trava esperando uma resposta que nunca vem, sem aviso. O prazo obrigatório é a saída estrutural: torna o timing visível e força a pergunta honesta “e se ele não responder?”. (O <- assíncrono não precisa, porque não espera.)

Por que supervisão emerge da estrutura? A BEAM tem link/monitor/trap_exit como primitivas cruas. Makoto não as expõe: a morte de um processo é um error comum (rodam os defers, libera-se o @mm, produz-se o motivo), e observar a morte sem morrer junto é literalmente o que o catch |e| de um spawn supervisionado já faz; um monitor não é construto novo, é o nome disso. Acoplar destino (links) é a árvore: processos no mesmo spawn-block vivem e morrem juntos porque a estrutura diz isso. As três estratégias (one_for_one, one_for_all, rest_for_one) caem da forma do código, não de um behaviour declarado à parte.

  • Threads + mutex + memória compartilhada (Rust/C++/Java). Reintroduz o aliasing concorrente que obrigaria o borrow checker, espalha a categoria “esqueci o lock”, e não escala para distribuído (mutex não atravessa a rede). Shared-nothing elimina a categoria de bug inteira.
  • O modelo Go puro (goroutines + sync). Go tem channels, mas também permite memória compartilhada e locks, e a porta do aliasing fica aberta. Makoto fecha: só channels, zero shared memory.
  • link()/monitor() crus (Erlang). Cascata de morte por links soltos é não-local: “quem matou quem?” fica implícito. A árvore de supervisão torna o destino acoplado visível na estrutura.
  • Um tipo de channel separado para rede / um framework de RPC. Reabriria a assimetria “local vs remoto”. Reusar Channel[T] com T + Serializable deixa a física (serialização, falha) se expressar pela constraint e pelo Result, sem um segundo modelo de comunicação.

O data race que só aparece em produção sob carga, porque alguém esqueceu um lock. O sistema que trava esperando uma resposta de rede que nunca chega, sem timeout, sem log. A cascata de mortes em Erlang onde um link() esquecido derruba metade do sistema e ninguém sabe o caminho. Cada uma é uma falha que o isolamento + timeout-obrigatório + árvore-de-supervisão tornam impossível ou visível.

Processos isolados conversando por channel, supervisionados. A morte é um valor que sobe ao supervisor; ele reinicia, derruba o grupo, ou escala, tudo emergindo da árvore do código.

Shared-nothing custa cópia/passagem de mensagem entre processos: você paga em throughput o que ganha em correção e em distribuição-nativa. A árvore de supervisão é mais rígida que link() cru: você só monitora os filhos que você spawnou; observação cross-tree vai por channel normal. Menos flexível que Erlang, mais estruturado de propósito.

Próximo: 05 · Erros