Erlang: processos concorrentes

Chegou a hora de vermos uma das features mais alardeadas pelo povo do Erlang: concorrência.

Primeiramente, vamos definir o que é concorrência e o que é paralelismo.

Na visão dos criadores de Erlang, concorrência é, meramente, você ter mais de um processo trabalhando na mesma máquina. Eles não precisam ser executados ao mesmo tempo. Se você tem uma CPU com um núcleo só e um sistema operacional multi-tasking, você deve ter vários programas sendo executados em condição de concorrência nesse exato momento. Mas cada um tem seu próprio “espacinho” na fila da CPU.

Eu comecei o parágrafo anterior com “na visão dos criadores de Erlang” porque sei que a internet tem um monte de gente babaca louca para cagar regra em cima de qualquer coisa que se diga. E eu não tenho mais paciência para ficar lendo comentários de gente assim. Tua definição de “concorrente” é outra? Teu professor da faculdade ou “um site” ou “um livro” dizem que é diferente? I DON’T CARE right now.

Paralelismo, por sua vez, é quando você tem vários programas ou várias partes de um mesmo programa sendo executados, literalmente, ao mesmo tempo, o que costuma ser o caso em CPUs multi-núcleos. Nesse exato momento, por exemplo, eu tenho 4 núcleos executando os diversos programas que estou usando agora.

Outra forma de paralelismo é ter vários programas rodando em vários computadores. Ou um mesmo programa rodando em vários computadores. Antigamente, este era a principal forma de se conseguir paralelismo. E, claro, há zilhões de programas rodando em paralelo, hoje, segundo essa definição (esse servidor web e o seu computador, per si, já entram nela). Mas ela é ampla demais do que “paralelismo como forma de resolver determinado problema em mãos”.

Acontece que Erlang, desde sempre, conseguiu trabalhar muito bem com concorrência. Mas paralelistmo dentro da mesma máquina só veio em 2006 (release da primeira VM estável com suporte a SMP). Curioso, não?

Então vamos logo abrir threads!

Devagar, pequeno gafanhoto, devagar. Acontece que a concorrência em Erlang (e só vamos controlar concorrência, agora, já que o paralelismo a gente deixará para a VM) acontece por meio de processos leves. E não, não são vários processos do sistema operacional, daquele jeito clássico que começa com um fork. A VM do Erlang tem seu próprio escalonador e é ela quem gerencia os processos do Erlang.

E por que é assim?

Bem, Erlang surgiu em um meio em que era necessário matar e iniciar novos processos de forma o mais imediata possível. E os forks dos sistemas operacionais são leeeeentoooos. Tá certo que, hoje em dia, nada é “lento”, mas qualquer gargalo, na época, era um senhor gargalo. Pelo menos para quem queria chegar nos “nove noves” de disponibilidade.

Os processos leves da VM do Erlang são, de fato, muito leves. Quando um processo é iniciado, seu footprint é de apenas 309 palavras de memória (sem SMP). Ou seja: mal passa de 2kB.

Além da velocidade, há uma vantagem enorme nos processos leves: a VM faz coleta de lixo separada e independente para cada processo. Ou seja: aquela situação de um programa congelar de quando em quando praticamente desaparece (contanto que você escreva seu programa direito). Em casos em que você quer controlar completamente a execução do processo, você pode simplesmente desabilitar a coleta de lixo automática e, se necessário (ou seja: se seu processo tem uma execução longa), pode chamar manualmente o coletor de lixo no momento mais adequado.

Ok, então. Processos…

Para iniciar um processo, usamos spawn/1. Usarei como exemplo um pequeno servidor que abre um socket e fica esperando conexões:

listen() -ᐳ
    listen(5000).

listen(Port) -ᐳ
    {ok, LSocket} = gen_tcp:listen(Port, [list, {packet, 0}, {active, false}, {reuseaddr, true}]),
    do_accept(LSocket).

do_accept(LSocket) -ᐳ
    {ok, Socket} = gen_tcp:accept(LSocket),
    spawn(fun() -ᐳ connection_handle(Socket) end),
    do_accept(LSocket).

Preste atenção ao momento em que spawn/1 é chamada. O socket fica ouvindo e, quando há uma conexão (primeira linha de do_accept/1), o programa lança um processo para lidar com a nova conexão e volta ao seu loop de espera.

Da parte do programa que lida com conexões, a ideia se resume em: quando alguém conectar, chama outro processo e “se vira aí com ela”.

Os processos podem ser ligados (linked) e monitorados. Um link é quando um processo depende do outro para sobreviver. Se um deles quebrar/cair/morrer inesperadamente (digamos, por causa de um bug) o outro morre também. Isso segue a filosofia de “se for morrer, morra o mais rápido possível e com o máximo de barulho”. Os dois processos morrem e o motivo do erro é “passado adiante”.

Os programas e/ou processes sempre devem morrer o mais rápido possível porque, quando o programa está “podre” ele pode corromper dados. Quanto mais tempo um programa podre viver, maior a probabilidade de que partes não-podres consumam dados também “podres” e, no final, sua base de dados ou a saída do programa em si sofra de um estrago muito grande.

Já o monitoramento é “mais leve”, e apenas informa se um determinado processo morreu e qual foi o motivo.

Resumo

Não vou adentrar agora os detalhes sobre processos, ligação, monitoramento, avisos de saída, etc. Por enquanto, brinque aí de abrir seus processos com seus próprios algoritmos e, assim que você sentir necessidade de mais controle eu volto para explicar tudo.


Este artigo foi escrito originalmente em 07/12/2014.