# Introdução

Já faz algum tempo que estou querendo escrever sobre concorrência, mas ~~minha procrastinação~~ minha rotina não deixou. E também porque estive ocupado o primeiro semestre de 2024 criando [uma](https://dev.to/leandronsp/construindo-um-web-server-em-assembly-x86-parte-i-introducao-14p5) [saga](https://dev.to/leandronsp/construindo-um-web-server-em-assembly-x86-parte-ii-historia-e-arquitetura-2jb9) de [artigos](https://dev.to/leandronsp/construindo-um-web-server-em-assembly-x86-parte-iii-codigo-de-maquina-bgk) [sobre](https://dev.to/leandronsp/construindo-um-web-server-em-assembly-x86-parte-iv-um-assembly-modesto-oif) [Assembly](https://dev.to/leandronsp/construindo-um-web-server-em-assembly-x86-parte-v-finalmente-o-server-9e5) [x86](https://dev.to/leandronsp/construindo-um-web-server-em-assembly-x86-the-grand-finale-multi-threading-24hp).

> Também não sei porque

Enfim, concorrência (*plus* paralelismo) é um assunto que gosto de explorar e conversar sobre, portanto chegou a hora de compartilhar um pouco da visão que tenho com relação a este tema.

Vá com moderação, não espere um guia acadêmico, formalidade e nem um tutorial rápido. O que pretendo aqui é trazer fundamentos de concorrência de **forma acessível e didática**, buscando lá na superfície do sistema operacional como isto funciona, tanto a nível de CPU, memória e I/O; e também como diferentes linguagens (leia-se *runtimes*) implementam concorrência.

E claro, não posso escrever um guia sobre concorrência sem começar com o famoso clichê: **concorrência não é paralelismo**.

> Pra não mencionar o *assincronismo*, que também vamos abordar ao longo deste guia

Se você sempre se deparou com explicações confusas, apresentadas de forma inacessível, ou então carregadas de jargões e até mesmo analogias que mais atrapalham que ajudam - calma gente, eu gosto de analogias, mas *boas* analogias -, então este guia é pra você.


# First things first

Muito do que vou trazer ao longo deste guia é um compilado de coisas que aprendi ao longo da carreira, tanto a nível acadêmico quanto profissional. Tem muito conhecimento teórico pra compartilhar aqui, mas também muito *empirismo*.

> Pegue minha visão com a devida moderação, e fique a vontade pra discordar de alguns pontos e me chamar pra conversar sobre

**Primeira coisa**: acho confuso falar de concorrência iniciando logo por *threads*. Isto geralmente vem acompanhado de "multi-threading", "paralelismo" e também em como deixar o sistema "mais rápido". Na carona vem palavrinhas que causam boa impressão como *escalabilidade e performance*.

Vamos com calma. Existem muitas camadas anteriores que é preciso sabermos.

> "Ah mas certas coisas não vão ajudar no dia-dia, nem vão pagar meus boletos"

*Wrong*. **Vão ajudar sim**. Não estou propondo alto conhecimento acadêmico e teórico, mas reflexões que vão te ajudar a, perdoem a analogia, *apertar aquele parafuso de forma eficiente quando ninguém mais na equipe conseguir apertar.*

E atenção para quando eu digo "eficiente": eficiente é o contrário de **caro e complexo**. Por vezes a gente acaba pensando numa solução que dá uma tremenda volta sendo que era só "apertar o parafuso". Abordagens como "é só colocar mais thread que fica mais rápido" acabam penalizando e muito a performance sem antes tomar a decisão, cientes das limitações da linguagem de programação, do framework, e inclusive do próprio sistema operacional.

E é por isso que resolvi escrever este guia.

## Ok, mas de qual sistema operacional estamos falando?

Quando o assunto é sistemas operacionais, diferentes implementações podem trazer diferentes conceitos, o que pode tornar o assunto impreciso. Por este motivo, é importante estabelecermos qual sistema operacional este guia abordará, e não só: a arquitetura do computador também.

Neste guia abordaremos mais precisamente conceitos da **arquitetura x86\_64 e sistema operacional UNIX-like**, podendo ser qualquer distribuição GNU/Linux ou MacOS, por exemplo.


# Agradecimentos

Não posso iniciar este guia sem deixar meus sinceros agradecimentos a todas as pessoas que têm me apoiado. O primeiro agradecimento vai para a **minha esposa Cassia Regina** que, com uma compreensão do tamanho do universo, tem me apoiado nesta jornada, desde o início quando "tudo isso era mato".

E aos demais apoiadores que, de alguma forma, contribuíram seja financeiramente ou com revisão de texto, para que este guia pudesse ser desenvolvido. Sem este apoio talvez eu não tivesse tanta motivação para escrever um guia desta magnitude. São pessoas assim que nos levam para a frente e nos motivam a fazer as coisas, então aqui vai por ordem cronológica:

```
### Investidores ###
Rodolfo De Nadai
Diego Antonio M Diniz
Carla de Oliveira Barden
David Alex Sylvestre
Carlos Eduardo Coelho B Shinagawa
Rodrigo Gonçalves Branco
Pedro Gomes Aciole da Silva
Rafael Martins C Ponte
Alexandre da Gama Lima
Jose Iedo L Duarte
Cassia Regina 
Emerson Vinicius Almeida
Lucas Perez
Daniel Lobão S Figueiredo
Lucas de Almeida Quaresma
Heitor Lessa
Lindolfo Rodrigues
Daniel Lacet de Faria
Lucas Giori
Iago Hisami Saito
Giovanni de Sousa Martins
Fabio Badaró
Caio Borghi
Thais Kusuki
Matheus Augusto S. Souza
Marcel Gonçalves dos Santos
Diego Abadan Moura
Hugo de Sousa Marques
Plinio Oliveira Ribeiro
Luis Pereira da Silva Netto

### Revisores ###
Alexandre Rocha
Emanuel Kidoguchi
```

*São 32 apoiadores fazendo isto acontecer*!&#x20;

## Buy me a coffee?

Se você gosta do meu trabalho com artigos e guias e quer fazer parte disto também, considera contribuir com o amigo aqui com aquele **cafezinho maroto** da amizade?

<figure><img src="/files/FUUkoZLpU19gvljXPbr1" alt="" width="283"><figcaption></figcaption></figure>

Ou copia e cola:

<pre><code><strong>00020126850014BR.GOV.BCB.PIX013638ee4bde-574b-4197-b10f-68742087b00b0223Gratidão pelo cafezinho5204000053039865802BR5925Leandro Freitas Maringolo6009SAO PAULO62140510qrN6Ov1wRl63041A3C
</strong></code></pre>

Fora este guia, já escrevi também outros guias e pretendo continuar com ainda mais, como por exemplo:

* [Guia de Web 101](https://web101.leandronsp.com/), que cobre todos os fundamentos da Web no geral
* [Guia AWS 101](https://aws101.leandronsp.com/), um guia que de forma bastante sucinta mostra como iniciar os primeiros passos criando instâncias EC2 de forma segura utilizando o CLI da AWS
* Pra não mencionar os **mais de 80 artigos** que escrevi na plataforma [DEV.to](https://dev.to/leandronsp)
* E meu [site oficial](https://leandronsp.com/), que têm alguns artigos mas que em breve irei dar uma repaginada, sendo o ponto central de todo o conteúdo técnico que produzo

> Pra quem tiver curiosidade em conhecer um "Leandro das antigas", ainda no início da minha carreira eu costumava escrever artigos técnicos. Ainda existe [essa pérola aqui](http://leandromaringolo.blogspot.com/) online, mas que não dou mais manutenção

***

Agora sim, sem mais delongas, vamos ao que interessa!


# O que é o programa no sistema operacional

No sistema operacional, cada programa é encapsulado dentro de uma estrutura chamada **processo**. Um processo é uma entidade independente, isolada dos demais processos. Essa separação permite que o SO gerencie **múltiplos processos simultaneamente**, atribuindo recursos como a CPU ou a memória de forma controlada. Sem o isolamento, teríamos enormes problemas com corrupção de dados pois diferentes processos iriam escrever na mesma *região na memória*.

<figure><img src="/files/dpSKMOniv6vuHcBHfGGr" alt="" width="317"><figcaption></figcaption></figure>

Repare na imagem de exemplo acima que a memória do computador é vista como uma "grande fita". Sendo o papel do sistema operacional alocar para cada processo uma área reservada nesta "fita", como podemos ver nas áreas verdes.

Esta organização em processos é que torna possível a *concorrência* no sistema operacional. E aqui temos então o início do conceito.

> Esqueça tudo sobre threads e outras palavras que fazem você se sentir mais sênior

Foca no *processo.*

Enquanto um processo utiliza a CPU para realizar cálculos ou executar instruções, outro pode estar aguardando uma operação de I/O, como a leitura de um arquivo ou o recebimento de dados da rede.

<figure><img src="/files/sHc1cOmOKlMu2r0GxnH8" alt=""><figcaption></figcaption></figure>

Essa alternância (também chamada de *troca de contexto*), gerenciada pelo sistema operacional, garante que o tempo da CPU seja aproveitado ao máximo, mantendo todos os processos "em movimento", mesmo que não estejam sendo executados simultaneamente.

Para entender melhor como o sistema operacional gerencia processos, podemos utilizar um comando muito sinistro que certamente muita gente já viu ao menos uma vez na vida: o comando **ps** que, de acordo com o manual - leiam o manual, sempre - , lista todos os processos ativos no sistema operacional:

```bash
$ ps

PID TTY          TIME CMD
31504 pts/0    00:00:00 bash
85358 pts/0    00:00:00 ps
```

Mas para listar todos os processos do usuário e expandidos, é preciso passar as opções *aux*:

```bash
$ ps aux

USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND       
root           1  0.2  0.3 167672 13156 ?        Ss   Nov24   7:05 /sbin/init    
root           2  0.0  0.0      0     0 ?        S    Nov24   0:00 [kthreadd]    
root           3  0.0  0.0      0     0 ?        I<   Nov24   0:00 [rcu_gp]      
root           4  0.0  0.0      0     0 ?        I<   Nov24   0:00 [rcu_par_gp]  
root           5  0.0  0.0      0     0 ?        I<   Nov24   0:00 [slub_flushwq]                                                       
root           6  0.0  0.0      0     0 ?        I<   Nov24   0:00 [netns]       
root           8  0.0  0.0      0     0 ?        I<   Nov24   0:00 [kworker/0:0H-events_highpri]
root          10  0.0  0.0      0     0 ?        I<   Nov24   0:00 [mm_percpu_wq]

.
.
.
.
```

> Dica: use o comando "man" para ler o manual de um determinado programa. Por exemplo, *man ls*. Inclusive, é possível ler o manual do manual, com *man man*, assim podemos aprender a interpretar o manual de forma geral

A lista é grande, pois existem centenas de processos **em execução** no meu sistema operacional.

Experimente também rodar o comando `htop`, que basicamente traz todos os processos do sistema com com uma visualização melhor e interativa:

<figure><img src="/files/xGj5pETRGaVdsnoq41qA" alt=""><figcaption></figcaption></figure>

Se reparar bem, o *htop* fica sempre atualizando a lista de processos que está "mais acima". Não importa agora para nós sabermos a prioridade de cada processo, mas é importante notarmos que há sempre uma **alternância**, feita pelo **escalonador de tarefas** do sistema operacional.


# Escalonador preemptivo de tarefas

O *escalonador de tarefas* do sistema operacional é o componente responsável por gerenciar o uso da CPU entre os processos, estabelecendo assim a **concorrência**. Por alternar tão rápido entre processos, temos a impressão que são executados simultaneamente - mesmo que na realidade, a CPU execute apenas um processo por vez em sistemas single-core.

Em sistemas UNIX-like, o modelo de escalonamento é chamado de **preemptivo**. Isso significa que o escalonador pode interromper a execução de um processo a qualquer momento para dar lugar a outro processo.

Essa *interrupção* é feita de forma controlada, pelo que o controle pode ser feito de algumas formas:

* **fatia de tempo (time slice)**: sinais que notificam o escalonador para alternar entre processos após um intervalo de tempo definido
* **prioridade**: a cada processo é associada uma prioridade, e processos mais prioritários podem ser escalonados antes de outros
* **estado dos processos**: o escalonador analisa se um processo está aguardando I/O, bloqueado ou pronto para ser executado

<figure><img src="/files/UOJJDYdGdDWUwa0pQmr5" alt=""><figcaption></figcaption></figure>

O escalonamento *preemptivo* permite ao sistema operacional controlar a concorrência de processos **sem que determinado processo monopolize a CPU**, balanceando o uso da CPU de forma eficiente.

> TIL: enquanto preparava o conteúdo deste guia, descobri dois comandos que permitem rodar programas modificando a prioridade em sistemas UNIX-like: **nice** e **renice**.

*Nice, uh?*


# Uma nota sobre escalonamento cooperativo

Até agora falamos de um modelo escalonamento que é o *preemptivo*, e que atende de forma eficiente muitos sistemas operacionais modernos.

Mas antes mesmo do modelo preemptivo se tornar predominante, muitos sistemas utilizavam o **escalonamento cooperativo**. Nesse modelo, cada processo controlava explicitamente quando liberar a CPU para outros processos. Isso era feito ao atingir certos pontos no código em que o processo voluntariamente "cedia" o controle ao sistema operacional.

> Daí o nome escalonamento *cooperativo*

Entretanto, o modelo cooperativo traz algumas limitações, pois um erro de programação como loop infinito por exemplo, pode fazer um processo *monopolizar* a CPU, caindo num cenário de **CPU starvation** para os outros processos.

Há também o fato de que o modelo cooperativo depende muito de uma **boa implementação** de cada programa, o que dificulta garantir tempos de resposta rápidos para tarefas críticas na CPU.

Mas calma, o modelo cooperativo tem suas vantagens...

O escalonamento cooperativo é usado em contextos específicos, como em sistemas embarcados simples ou em algumas linguagens de programação ou frameworks/bibliotecas que implementam **concorrência cooperativa** com o modelo de *corrotinas* - que vamos abordar mais a frente no guia.

***

> Bom, se você prestou bem atenção até aqui, eu não fiz uma menção sequer a *threads*. É proposital, acho que devemos dissociar este conceito de concorrência às threads (em um primeiro contato)


# Propriedades de um processo

O sistema operacional enxerga tudo como uma unidade de concorrência. Ou seja, o teu programa, que está encapsulado em um processo, é uma unidade de concorrência. Vamos relembrar as propriedades básicas de um processo UNIX-like:

* processos têm estado privado e não compartilham memória
* processos se comunicam uns com os outros por envio de mensagens - IPC, ou Inter-Process Communication -, que podem ser pipes, arquivos, FIFO, sockets etc (veja [este meu artigo sobre UNIX pipes](https://dev.to/leandronsp/series/18468) para entender mais sobre IPC)
* processos têm um identificador único no sistema (PID)

<figure><img src="/files/7gzqwHoqGjQT8OcFvv67" alt="" width="375"><figcaption></figcaption></figure>

***

Há situações em que nosso programa precisa escalar para atender uma determinada demanda, como por exemplo requisições que chegam no sistema.

Se pararmos pra refletir um pouco, uma forma de escalar seria aumentar o "número de programas" do nosso sistema, correto? Essa técnica baseia-se em replicar o mesmo processo N vezes, como se fossem *clones*.

Pois é, o sistema operacional fornece uma syscall chamada **clone**.


# Clone de processo (forking)

A *syscall \*\*clone*\*\* sendo chamada *sem argumentos*, permite criar um outro processo filho exatamente igual, mas como processos são isolados, então todos os dados e programa **seriam copiados para outra área da memória**. Desta forma, poderíamos ter várias réplicas (filhos) do programa (pai) de modo a atender a uma demanda de acessos.

Esta técnica é chamada também de *forking de processos*.

<figure><img src="/files/kNITaPepNjbbVJYNUQq6" alt=""><figcaption></figcaption></figure>

Entretanto, *forking* pode fazer com que o uso de memória do computador atinja o limite máximo mais depressa, pois cada fork (filho) vai ter uma cópia exata do processo original (pai).

Forking é uma técnica bastante utilizada, principalmente em cenários de **escalabilidade** que demandam o cenário que foi trazido de aumento do número de requisições no sistema.

> Eu disse que não ia citar a palavra *escalabilidade* mas é natural falar de escalabilidade quando estamos falando de concorrência. Por isso não vejo este termo como algo "complexo". Uma vez entendendo sobre concorrência de verdade, fica mais fácil resolver problemas de escalabilidade
>
> E sim, eu posso futuramente escrever um outro guia falando só sobre escalabilidade de sistemas, mas vai ser inevitável falar de concorrência. Bem, você entendeu...

Okay, e se eu te disser que ao invés de clonar *todos os dados do programa (processo)*, eu quiser clonar apenas uma parte dele, fazendo com que esta parte seja mais **leve** e tenha **acesso compartilhado** com a memória do processo principal?

O sistema operacional oferece este recurso?

Bem, para nossa sorte sim. A mesma chamada *clone*, se receber alguns argumentos específicos, faz exatamente isso que queremos: um "processo filho" mais leve que compartilha a memória do processo principal com outros processos "irmãos".

A este processo "leve" damos o nome de **thread**.

> Não precisa se assustar e apegar a detalhes técnicos que vêm a seguir, pois quero apenas elucidar estas estruturas primitivas no sistema operacional


# Clone leve de processo (thread)

Vamos viajar aqui, como seria a chamada `clone` no sistema operacional, com pseudo-código:

```
int clone() {
   // forking de processo, sem argumentos
   // vai criar um outro processo filho copiando tudo o que está na memória
   // para outra região da memória
}

// Mas para criar um processo mais "leve"

int clone(**args) {
   // recebe os argumentos específicos e cria o processo filho mais leve
   // ou seja, cria uma THREAD :)
}
```

Para criar um processo mais leve, ou seja uma **thread**, seria então necessário passar os seguintes argumentos para a chamada `clone`:

* **CLONE\_VM**: processo principal e processo filho compartilham o mesmo espaço de memória virtual
* **CLONE\_FS**: processos compartilham o mesmo sistema de arquivos
* **CLONE\_FILES**: processos compartilham a mesma tabela de descritor de arquivos (file descriptor table)
* **CLONE\_SIGHAND**: processos compartilham a mesma tabela de handlers de sinais (signal handlers)
* **CLONE\_PARENT**: processos compartilham o mesmo parent, ou seja, o processo "filho" na verdade é filho do processo parent do processo original (mesmo porque estamos falando de uma thread que compartilha o mesmo processo)
* **CLONE\_THREAD**: o processo filho é colocado no mesmo grupo de threads do processo original
* **CLONE\_IO**: processos compartilham o mesmo contexto de I/O

<figure><img src="/files/U1gYKm9yZ4BHCvGSMeYr" alt="" width="563"><figcaption></figcaption></figure>

É isto, caros leitores, uma thread é uma "parte" de um processo, mais leve e que compartilha a memória do processo com outras threads.

Como o sistema operacional é nosso amigo, ele trata as threads como **unidades de concorrência**, assim como os processos, portanto diferentes partes (threads) de um programa podem ser executadas de forma concorrente no sistema, assim como diferentes processos:

<figure><img src="/files/VRBZk2Tw7WoHOA69LUcJ" alt=""><figcaption></figcaption></figure>

Repare como que o escalonador fica *intercalando* tanto processos, quanto threads na CPU. **Que dia lindo, não?**


# Todo processo tem uma thread principal

Outra coisa para termos em mente, e que é *muito importante*, é o fato de que cada processo incorpora uma thread principal, chamada de **main thread**. Ou seja, quando o processo está em execução, lhe é atribuído um ID de thread (TID) que ocorre de ser o mesmo ID do processo (PID):

```bash
$ ps -eo pid,tid,cmd | grep system | head -n 5
	PID     TID CMD
    387     387 /lib/systemd/systemd-journald
    428     428 /lib/systemd/systemd-udevd
    536     536 /lib/systemd/systemd-timesyncd
    584     584 /lib/systemd/systemd-networkd
    586     586 /lib/systemd/systemd-resolved
```

Repare, que desta forma, fica mais prático para o sistema operacional tratar tudo como "thread", no fim das contas.

> Na verdade, no `libc` existe uma estrutura de dados que se chama **task** e é usada justamente para isto. Mas em termos de escalonamento, podemos chamar de "thread" de modo a simplificar

Até agora falamos apenas de CPU single-core. Mas todos sabemos que, a partir dos anos 2000, tivemos um aumento exponencial do uso de multi-core. Cada vez mais as CPU's são projetadas para terem **mais de um núcleo** de execução.

O que muda no nosso modelo de concorrência? De forma *simplista*, nada. Mas se pensarmos que agora temos mais de um núcleo de execução, podemos **PARALELIZAR** a exeução de diferentes processos e threads consoante ao número de núcleos disponíveis.


# Uma nota sobre paralelismo

Eu não queria gastar muito texto pra falar de paralelismo, mesmo porque paralelismo é o ato de *rodar coisas em paralelo*. Nada mais do que isso. Mas pra isso, precisamos de hardware.

Ou precisamos de mais CPU, ou precisamos de mais núcleos de CPU. Simples assim.

Entretanto, há uma vantagem do paralelismo que ainda não mencionei mas que dá pra falar brevemente. Lembra do problema de escalabilidade que poderia ser resolvido com **clone de processos**? Pois bem, com um núcleo apenas, ainda assim só conseguimos executar uma thread por vez de forma concorrente.

Supondo que temos 400 operações pra executar, e nossa CPU sozinha consegue fazer 100 por segundo, teríamos então uma capacidade de terminar todas as operações em 4 segundos.

Esse é o nosso **throughput**, ou seja, a capacidade de realizar N tarefas num determinado intervalo de tempo.

> 100/ops por segundo

Porém com uma CPU **dual-core**, podemos executar 2 operações por vez de forma paralela, assim nosso throughput dobraria, para 200/ops por segundo.

<figure><img src="/files/zmpVyqlAJfs2JQWO1HxC" alt=""><figcaption></figcaption></figure>

Bom, é isso que queria falar sobre paralelismo. Não tem segredo. Quer aumentar o throughput do sistema? *Coloca mais hardware*.

Mas calma jovem...dá pra aumentar throughput também sem precisar ficar comprando mais hardware (que tá cada vez mais caro, né?). Por isso eu bato tanto na tecla para dissociarmos concorrência de paralelismo.

Voltando ao foco principal do guia: **concorrência.**


# Principais desafios em cenário de concorrência

Quando temos um fator externo decidindo quando *alternar* as tarefas/threads, não temos qualquer controle sobre a ordem com que estas threads serão executadas.

Seja por time-slice, por prioridade ou até mesmo espera no I/O, a decisão do escalonador de quando uma thread vai ser executada e em qual ordem, não está sob nosso controle.

<figure><img src="/files/4lnkTkYqIVdLI8tC9xWT" alt=""><figcaption></figcaption></figure>

Na imagem acima podemos ver que, apesar de T1 ter iniciado antes que a T2, só foi concluída depois. No meio do processo - e por algum motivo *que nunca vamos saber qual e nem queremos saber* -, o escalonador decidiu interromper a execução da T1 pra priorizar a T2 até sua conclusão.

> Vai entender, é cada doido nesse mundo...

Então com isso adicionamos uma variável que torna o mundo da concorrência tão desafiador.


# Race condition

Se nosso programa tem 2 threads que fazem uso de um recurso compartilhado (como memória ou até mesmo arquivo no disco), e o *estado final do recurso compartilhado* depende da ordem de execução das threads, então temos um problema de **race condition**, ou *condição de corrida*.

<figure><img src="/files/ipc5z4nrDQP55H4SHJHV" alt="" width="563"><figcaption></figcaption></figure>

Condição de corrida é um problema devido ao uso de concorrência, pois não conseguimos determinar **se e quando** uma thread será executada.&#x20;

## Quando race conditions se tornam data races

Quando o recurso compartilhado for a **memória do programa**, e *uma das threads estiver tentando fazer uma operação de escrita*, podemos ter uma sub-categoria mais específica de race condition que é **data race**.

Data races são mais graves pois podem gerar corrupção na memória do programa, afetando seu funcionamento e até causando segmentation fault dependendo do caso. Para evitar data races, o uso correto de *mutexes, semáforos ou barreiras* deve ser levado em conta.

***

Sabendo dos problemas inerentes a concorrência, temos que ter em mente que, sempre que tivermos um cenário de race condition, precisamos **sincronizar o acesso** ao recurso.&#x20;

E a sincronização precisa ser feita mediante a implementação de **locks.**


# Sincronização com locks

O sistema operacional fornece algumas primitivas para construção de mecanismos de sincronização (locks), como **futex** e instruções atômicas. Entretanto, essas primitivas são muito genéricas e requerem uma implementação mais robusta de lock, pois o Kernel obedece um design minimalista e tenta não impor nenhum mecanismo de lock, deixando que diferentes linguagens de programação possam implementar diferentes tipos de locks.

Vários tipos de locks podem ser implementados, mas vamos explicar brevemente o conceito de dois: *spinlock e mutex*.

### Spinlock

Um lock baseado na técnica "busy-waiting", que é precisamente um loop onde a thread verifica o valor de uma variável compartilhada atomicamente. O spinlock não causa **troca de contexto** de threads, mas pode provocar um aumento do uso de CPU devido ao loop.

### Mutex

Mutex refere-se a *mutual exclusion*, ou **exclusão mútua**, onde a thread é colocada em um estado de (wait) até que o recurso seja liberado por outra thread (wake). Isto pode ser implementado por meio de *thread signaling*, que são basicamente sinais que podemos enviar às threads.

A vantagem é que a CPU não é consumida durante o tempo de espera da thread, porém há uma latência maior devido à **troca de contexto** das threads. E também pode-se gerar um problema onde duas ou mais threads ficam bloqueadas pra sempre por conta de um lock mal utilizado ou "perdido", que se chama **deadlock**.

<figure><img src="/files/XqKWw2arnD5H5LLnwrvF" alt="" width="258"><figcaption></figcaption></figure>

&#x20;Com mutex, podemos fazer com que a thread que chegou primeiro tenha acesso exclusivo ao recurso. Qualquer outra thread que tentar acessar o mesmo recurso, **seja pra leitura ou escrita**, vai ter que esperar o lock ser liberado...

<figure><img src="/files/Q6p5s8xek2HniEifadCu" alt="" width="563"><figcaption></figcaption></figure>

Existem outros tipos de locks, como **semáforos e rwlocks**, mas por enquanto vamos deixar apenas esses dois, pelo que na segunda parte do guia abordaremos em detalhes a implementação de locks em cada linguagem.

***

Entretanto, podemos implementar alternativas em cenários de concorrência de modo a evitar o uso de locks. Quando o compartilhamento de memória e uso de locks passa a ser um grande problema, uma alternativa é implementar algumas "peças" de concorrência onde o e**stado não é compartilhado** e a comunicação entre diferentes peças passa a ser através do **envio de mensagens**.\
\
Estamos falando do **modelo de atores**.


# Modelo de atores

Com modelo de atores podemos modelar nosso sistema concorrente onde cada unidade de execução é um ator:

* possui identificação única
* possui estado privado (não compartilha estado), ou seja, apenas o ator é responsável por modificar seu estado interno
* se comunica através do envio de mensagens, seja por meio de canais, filas, etc

<figure><img src="/files/4JefWfLeVsTcO0Jvd52a" alt=""><figcaption></figcaption></figure>

Como podemos ver na imagem acima, é possível concluir que um ator é  bastante semelhante a um *objeto em OOP*.

> Me perdoem

A grande diferença é que o *ator é feito exclusivamente para cenários de concorrência*, onde a implementação do ator pode ser baseada tanto em kernel threads quanto user threads, caso o runtime tenha a implementação.

O ponto principal para entendermos aqui é que o **estado não é compartilhado**, ou seja, é como se tivéssemos uma cópia única de todos os atributos do ator em diferentes threads mas com diferentes valores, aumentando assim o uso de memória total do sistema.

Com atores, *eliminamos* a necessidade de sincronização com locks.

> Há vantagens e desvantagens em ambas as abordagens, lembre-se de que não existe bala de prata


# E o I/O?

Até aqui, só falamos de CPU e memória. É CPU pra cá, memória pra lá, concorrência, threads, escalonador...mas e quando o programa precisa de algo **fora da CPU**, como um arquivo no disco ou uma mensagem que chega pela rede?

Aí entra o famoso **I/O** (*Input/Output*), que deve ter um capítulo à parte nessa história de concorrência.

> Mas espera aí, o que isso tem a ver com concorrência?

Tudo, chefia. Vamos entender.

Enquanto a CPU está lá, super rápida e fazendo suas contas *crazy* em *nanosegundos*, o mundo do I/O é como uma fila do banco numa segunda-feira: **lento e cheio de esperas** (lá ele com *analogias*). Um disco rígido, por exemplo, demora *milisegundos* para responder – e isso é uma eternidade pra CPU.


# Latência de CPU vs Latência de I/O

Há uma **diferença na ordem de grandeza** entre diferentes dispositivos de hardware. Os ciclos de CPU tendem a ser os mais rápidos, enquanto que operações em I/O tendem a ser mais lentas. Claro que no final tudo depende do tipo de hardware e fabricante, mas no geral podemos aplicar a seguinte hierarquia nesta ordem de grandeza (apenas exemplo, não se apegue tanto aos números):

<figure><img src="/files/SlASbRCKk0FhcksaxCKE" alt=""><figcaption></figcaption></figure>

Numa escala relativa, o acesso à memória RAM é 300x mais lento que o ciclo da CPU, ao SSD é 300 mil vezes mais lento e ao HDD é **33 milhões de vezes mais lento**. Então a pergunta que fica é: *o que a CPU faz enquanto espera o I/O?*

Ou melhor: **o que o programa faz enquanto espera I/O?**

É aqui que o I/O começa a ditar o ritmo da música. Dependendo de como o programa interage com I/O, ele pode:

a) bloquear esperando I/O responder, ou&#x20;

b) ser super esperto e dizer pro sistema operacional "hey me avisa quando você tiver pronto o que te pedi no I/O, ok?"


# Chamadas bloqueantes

Quando um programa precisa fazer uma chamada no I/O, a CPU sofre uma interrupção para ser liberada para outro programa do computador e o processo atual fica *bloqueado*. É por isso que chamamos de **I/O bloqueante**.

<figure><img src="/files/94UhG8QecvVgQmNBgROJ" alt="" width="563"><figcaption></figcaption></figure>

Durante a leitura do arquivo, a thread não faz outra coisa. Se for a thread principal do processo, o programa todo fica bloqueado até que a leitura do I/O seja concluída.

Dependendo das características do sistema, isto pode trazer problemas de performance. Se por exemplo, múltiplos requests HTTP chegam no servidor, a latência total da aplicação será muito alta, fazendo com que diversos requests fiquem travados na fila.

Uma solução? Disparar uma thread para cada requisição HTTP:

<figure><img src="/files/RW5OR0yQH2bOUlYInrWI" alt="" width="563"><figcaption></figcaption></figure>

Desta forma, enquanto uma thread fica bloqueada esperando I/O, outra pode fazer uso da CPU ou mesmo de outro recurso de I/O.

> Yay! Problema resolvido, certo Leandro?

Calma, *jovem*.

Problemas com esta abordagem: a criação de thread no SO tem seus custos. No caso de termos milhares de requisições, terminaríamos com a criação de milhares de threads, apenas para atender as requisições HTTP de um sistema. E quanto aos outros programas do computador?

O sistema operacional define um **limite de threads** para serem criadas. Criar thread não deveria ser algo banal, temos que criar threads com muita parcimônia.

Uma forma de resolver esta criação absurda de threads é definir uma "piscina" de threads, com um número limitado, onde estas threads seriam recicladas para todos os pedidos que chegassem na aplicação.

Sim, estamos falando de **pool de threads**.

<figure><img src="/files/GAvq7mx1muhBP3bsk1Yt" alt="" width="563"><figcaption></figcaption></figure>

A definição de uma pool de threads pode ser implementada com qualquer linguagem de programação. Basta ter uma fila, onde os pedidos são enfileirados; e depois a definição de **um número fixo de threads** que ficam consumindo os pedidos da fila. Ao término, a thread volta a esperar algo na fila.

Pool de threads resolve a limitação de criação de threads no SO e evita com que utilizemos recursos de forma desnecessária. Entretanto, para lidar com chamadas no I/O, precisamos mesmo bloquear as threads?

E se houvesse um mecanismo no SO, bem inteligente, que permite que as chamadas no I/O possam ser *não-bloqueantes*, notificando assim o programa quando o recurso ficar pronto?


# Chamadas não-bloqueantes

O sistema operacional fornece um recurso muito interessante de chamadas não-bloqueantes em I/O. Desta forma, o programa recebe imediatamente um *file descriptor* que representa o I/O solicitado, e depois recorre a recursos do sistema operacional para verificar quando o recurso está pronto. Assim, o programa não fica bloqueado.

Esta é a definição de **assincronismo**, ou **I/O assíncrono**.

<figure><img src="/files/RveM1iRybVAsd4KMmJgv" alt="" width="563"><figcaption></figcaption></figure>

Em Linux, chamadas de sistema como **select** ou **epoll** permitem controlar I/O assíncrono. Com select o programa verifica quais descritores estão prontos, enquanto que no epoll, o SO notifica o programa através de uma fila quais descritores estão prontos.

> Também não vamos por enquanto entrar nos detalhes do uso de select ou epoll, isto fica numa seção futura neste guia quando entrarmos na parte de implementação. Aqui, é importante *entendermos os conceitos*.

De qualquer forma, o programa, seja ele qual for, não precisa usar multi-thread para lidar com I/O - a não ser que realmente queira. Com apenas uma thread, é possível criar um "loop" que fica verificando no SO quais chamadas ficaram prontas.

Vamos, em pseudocódigo, escrever um loop com este propósito:

```
fun main():
    # Lista de descritores de arquivos
    
    input_fds  = [fd1, fd2, fd3]  # Descritores para monitorar leitura
    output_fds = [fd4]            # Descritores para monitorar escrita
    error_fds  = []               # Descritores para monitorar erros

    while true:
        # Chama a função select e espera até que algum descritor esteja pronto
        ready_read, ready_write, ready_error = 
	        select(input_fds, output_fds, error_fds)

        # Verifica os descritores prontos para leitura
        for fd in ready_read:
            data = read(fd)
            print("Dados lidos de fd:", fd, "->", data)

        # Verifica os descritores prontos para escrita
        for fd in ready_write:
            write(fd, "Mensagem de teste")
            print("Dados escritos em fd:", fd)

        # Verifica os descritores com erro
        for fd in ready_error:
            print("Erro detectado no descritor:", fd)

```

Enfim, a ideia aqui é ilustrar como seria um **loop de eventos** hipotético, que tanto ouvimos falar:

* iniciar loop
* passar para o select (ou epoll) a lista de descritores que queremos monitorar
* ler os descritores que estão prontos para leitura
* escrever nos descritores que estão prontos para escrita
* repetir o loop infinitamente

Repare que, ao termos um loop assíncrono de eventos, é extremamente importante toda e qualquer chamada ser **não-bloqueante**, caso contrário, se tivermos ao menos uma chamada bloqueante, o loop todo ficará bloqueado, e consequentemente a thread ficará bloqueada.

> Lembrem-se, com I/O assíncrono nunca podemos bloquear o loop. Toda chamada no I/O deve ser assíncrona.


# Assincronismo e escalonamento cooperativo

Escrever código assíncrono pode levar a alguns desafios na forma como *escrevemos o código* do programa. A natureza assíncrona nos obriga a definir estruturas que encapsulam a lógica a ser executada em outro contexto (callbacks), quando o I/O ficar pronto, o que pode levar a múltiplos callbacks chamando outros callbacks de forma aninhada, causando o que chamamos de **callback hell**.

> Nenhuma linguagem está livre disto, pensando de forma abstrata

```
fn processData(callback):
  callToAction(callback() {
  }, anotherCallback {
  }, yetAnotherCallback {
  })...
```

E assim por diante...

Para mitigar este problema de *indireção com assincronismo*, podemos implementar técnicas funcionais como *monads* que lidam com tais efeitos colaterais. Por exemplo, estruturas como "async" e "await" presentes em algumas linguagens são uma forma de deixar o código mais "síncrono", encapsulando todo o efeito colateral do assincronismo em uma estrutura interna.

Desta forma, o código fica mais simples de manter, mitigando o problema com *callback hell*.

Outra forma de lidar com este problema é criando estruturas que permitem *devolver o controle* para o fluxo principal quando uma chamada de I/O assíncrono for identificada. Abstrações como **corrotinas** e **fibers** são vistas em diferentes linguagens para lidar com este problema.

> Lembra do escalonamento cooperativo que vimos mais cedo no guia?

Então, com escalonamento cooperativo podemos fazer com que diferentes unidades de concorrência dentro do processo (coroutines ou fibers) possam cooperar com o uso de recursos, no caso I/O.

<figure><img src="/files/lf6qACQbfuiCcqU8EzV8" alt=""><figcaption></figcaption></figure>

* a corrotina delega controla para o loop com "yield", o que significa que ela ficará a espera de I/O
* o loop não fica bloqueado e pode atender a outras corrotinas
* quando o I/O fica pronto, o loop chama o "resume" na corrotina, devolvendo o controla e ela

Desta forma, I/O assíncrono e escalonamento cooperativo são grandes aliados para resolver o problema de **assincronismo**.

***

### Está gostando deste meu trabalho? Buy me a coffee?

Se até aqui gostou do que escrevi, considera me pagar um cafezinho maroto? Gratidão que define :heart:

<figure><img src="/files/FUUkoZLpU19gvljXPbr1" alt="" width="283"><figcaption></figcaption></figure>

Ou copia e cola:

```
00020126850014BR.GOV.BCB.PIX013638ee4bde-574b-4197-b10f-68742087b00b0223Gratidão pelo cafezinho5204000053039865802BR5925Leandro Freitas Maringolo6009SAO PAULO62140510qrN6Ov1wRl63041A3C
```


# Vamos colocar em prática...

Até o momento, exploramos os conceitos fundamentais para compreender *concorrência*. Esses tópicos incluem:&#x20;

* **O que é um programa a nível de SO**: como os programas interagem com o SO
* **Propriedades de um processo**: estrutura, estado e formas de comunicação entre processos
* **Escalonamento preemptivo de tarefas**: como o SO gerencia a execução e troca de tarefas
* **Clone de processos (forking)**: como novos processos são criados
* **Clone de processos leves (threading)**:  diferenças entre criação de processos e threads
* **Uma nota sobre paralelismo**: concorrência **não é** paralelismo
* **Desafios em cenário de concorrência**: problemas comuns como condições de corrida e data races
* **I/O síncrono e assíncrono**:  como estas operações afetam o desempenho e design de sistemas concorrentes

Com isto tudo que foi passado, temos já o conhecimento necessário para entrarmos na *segunda parte* deste guia, trazendo exemplos práticos que implementam concorrência em diversas linguagens.

Não se preocupe, irei trazer exemplos auto-contidos e didáticos de forma que não será necessário entender todas as linguagens de forma profunda.&#x20;


# Definindo ambientes de execução

Antes de falarmos das diferenças entre algumas *implementações* de **linguagens de programação**, precisamos **definir** o que é uma linguagem de programação.\
\
De acordo com a Wikipedia:

> A linguagem de programação é um método padronizado, formado por um conjunto de [regras sintáticas](https://pt.wikipedia.org/wiki/Sintaxe) e semânticas, de implementação de um [código fonte](https://pt.wikipedia.org/wiki/C%C3%B3digo_fonte) - que pode ser [compilado](https://pt.wikipedia.org/wiki/Compilador) e transformado em um [programa de computador](https://pt.wikipedia.org/wiki/Programa_de_computador), ou usado como [script](https://pt.wikipedia.org/wiki/Linguagem_de_script) [interpretado](https://pt.wikipedia.org/wiki/Linguagem_interpretada) - que informará [instruções](https://pt.wikipedia.org/wiki/C%C3%B3digo_de_m%C3%A1quina) de processamento ao [computador](https://pt.wikipedia.org/wiki/Computador).

Ou seja, quando falamos em **linguagem de programação**, estamos nos referindo a uma especificação sintática e semântica que, através de um processo de **compilação** (ou interpretação), será transformado em código de máquina que o computador possa entender.

Para que uma linguagem seja executada no computador, é preciso ser implementada através de um programa, ou conjunto de programas e ferramentas, formando um "ambiente", onde será feito o processo de compilação, interpretação e/ou execução. Estas instruções irão utilizar **recursos do sistema operacional** que, por sua vez, irá manipular recursos de hardware, como CPU, memória e I/O.

<figure><img src="/files/hubTNMQkeAPpNWAICASq" alt="" width="563"><figcaption></figcaption></figure>

> Com isso conseguimos entender porque é tão importante a primeira parte deste guia, amém?

## Compiladores

Compilador é um programa que, dado um input que é código-fonte que segue a especificação de uma linguagem, aplica transformações e gera um executável que contém código de máquina da arquitetura do computador ou um código intermediário.

Exemplos de linguagens que utilizam compiladores: C (GCC, Clang),  Java (javac), Kotlin (kotlinc), Go (gc), Rust (rustc)&#x20;

<figure><img src="/files/LHC2SIvMstrhmvZEVAFz" alt="" width="563"><figcaption></figcaption></figure>

É possível haver mais de uma implementação para cada linguagem de programação. Vai depender da adesão, investimentos feitos entre outros aspectos.

## Interpretadores

Interpretador é um programa que, dado um input que é código-fonte que segue a especificação de uma linguagem, aplica transformações e executa as instruções da arquitetura "em tempo real".

Exemplos de linguagens que utilizam interpretadores: Python (CPython), Ruby (CRuby), PHP (Zend), Javascript (V8), Java (JVM)

Assim como no caso dos compiladores, é possível haver mais de uma implementação de interpretador para cada linguagem:

<figure><img src="/files/vY9vPM7Ci7spWToSoDtG" alt="" width="563"><figcaption></figcaption></figure>

Repare na principal diferença entre interpretador e compilador: o compilador gera um executável, enquanto que o interpretador vai traduzindo e executando diretamente. Há vantagens e desvantagens em ambos os casos, inclusive há diversos ambientes que fazem um misto de processo de compilação com um interpretador embutido.

## Vai um Javinha aí?

Por exemplo, em Java, o compilador `javac` não gera um código de máquina da arquitetura diretamente, mas sim um "bytecode" intermediário. Então, o bytecode é executado por um interpretador que vem dentro do ambiente Java. A este ambiente de execução (*runtime*), que é capaz de interpretar os bytecodes gerados pelo `javac`e traduzir para instruções da arquitetura do computador, chamamos de *Java Virtual Machine*, ou simplesmente **JVM**.

<figure><img src="/files/G6790aZhoDMQ5qIswvNO" alt=""><figcaption></figcaption></figure>

## E por quê precisamos saber disto com relação a concorrência?

Como vimos na primeira parte do guia, o sistema operacional oferece recursos (chamadas de sistema,  **syscalls**) para criar processos, threads e gerenciar recursos de I/O:

* criação de processos e threads por meio da syscall **clone**
* gerenciar recursos de I/O utilizando as syscalls **read**, **write**, **open** etc
* gerenciar recursos de I/O assíncrono com as syscalls **select**, **epoll**, **io\_uring** etc

Ou seja, as implementações de linguagens de alto nível precisariam fazer estas chamadas. E, pra nossa sorte, praticamente **todas as linguagens mainstream** (de propósito geral) fornecem formas de fazer estas chamadas ao sistema operacional.

> Seria muito estranho uma linguagem não permitir a chamada de syscalls, né? Pra quê serviria esta linguagem então, se não fosse pra chamar syscalls no SO com o intuito de manipular recursos de hardware?

## Ambientes de execução (runtimes)

Algumas implementações acabam por ter um conjuto de compilador com interpretador, como vimos no exemplo do Java. E não só, podem também trazer técnicas de alto nível para criação de "user threads" (que são threads que vivem apenas dentro do ambiente, e não threads do SO), garantindo a troca de contexto entre elas e sua execução, tudo dentro do ambiente.

Geralmente, estes ambientes se chamam **runtimes**. Poderíamos tecnicamente chamar tudo de runtime, mas em alguns casos isto não é precisamente correto, como no caso do GCC, que é apenas um conjunto de ferramentas para compilação de código C.

Entretanto, podemos acordar, neste guia, que iremos tratar tudo por "runtime" (ambientes de execução), não importa se é apenas um compilador, um interpretador, um híbrido, se implementa user threads  ou não, enfim. Vou chamar tudo de *runtime* simplesmente.

> Me perdoem, acadêmicos de plantão. Não é hoje que vocês vão me cancelar

Aqui vai uma lista das principais implementações de algumas linguagens que vamos abordar ao longo deste guia:

* C (GCC)
* Java JDK (javac + JVM)
* Python (CPython)
* Ruby (CRuby, ou MRI)
* Javascript (V8, usado no Chrome e também no Node.js)
* PHP (Zend)
* Go (gc)
* Rust (rustc)
* Kotlin (kotlinc + JVM)
* Elixir (BEAM)

Acho que com estas linguagens dá pra cobrir diversas funcionalidades de concorrência. Iremos entender as principais diferenças, casos de uso e suas vantagens/desvantagens. Vamos fazer uma viagem longa através dos processos, criação de threads no SO, criação de threads dentro do runtime, manipulação de I/O assíncrono entre outras coisas interessantes.

Aperte os cintos, porque agora sim, vamos começar a ver o negócio na prática!


# Concorrência em C

Para entendermos concorrência, nada melhor do que começar do nível mais baixo, certo?

> Calma, não vou falar de Assembly aqui, mas se você quiser entender mais de Assembly, mais especificamente x86, sugiro a leitura da minha saga escrevendo um [web server em x86](https://dev.to/leandronsp/construindo-um-web-server-em-assembly-x86-parte-i-introducao-14p5).

Nesta seção, iremos abordar como a linguagem C resolve os problemas de concorrência trazidos na primeira parte, seja a nível da implementação padrão ou bibliotecas externas.

## Preâmbulo

A implementação de C mais amplamente utilizada é o **GCC (GNU Compiler Collection)**, que oferece um conjunto de ferramentas para compilação, otimização e depuração de código. Essa popularidade se deve à sua maturidade, portabilidade e constante evolução, tornando-o o padrão de fato no ecossistema C.

Os exemplos aqui trazidos serão a partir de um Ubuntu 22.04.4 LTS (jammy) com gcc 11.4.0.

```
$ lsb_release -a
Distributor ID: Ubuntu
Description:    Ubuntu 22.04.4 LTS
Release:        22.04
Codename:       jammy

$ gcc --version
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
Copyright (C) 2021 Free Software Foundation, Inc.
```

No que diz respeito à concorrência, a linguagem C fornece acesso direto a syscalls do sistema operacional, como `fork` e `clone`, que permitem a criação de processos e threads, respectivamente. Mas essas chamadas diretas exigem cuidado na manipulação, já que expõem a complexidade do sistema operacional, tornando o uso muito mais complexo.

### libc

Para nossa sorte, a *biblioteca padrão* de C — frequentemente referida como **libc** — oferece abstrações de mais alto nível para facilitar o trabalho com concorrência, como a função `pthread_create`, usada para gerenciar threads de maneira mais simples e segura.

Além disso, a **libc** fornece mecanismos para lidar com sincronização, como **locks** (mutexes), permitindo o controle do acesso compartilhado entre threads.

### I/O assíncrono

Quando o assunto é **I/O assíncrono**, não há uma abstração completa na biblioteca padrão, mas syscalls como `select` ou `epoll` podem ser utilizadas para monitorar múltiplos descritores de arquivo e implementar loops de eventos personalizados.

### Bibliotecas externas

Assim como praticamente toda linguagem de programação, o ecossistema C desenvolveu diversas bibliotecas externas de código aberto (*open source*) para preencher lacunas que faltam no core da linguagem ou na biblioteca padrão.

Seja no contexto de **I/O assíncrono**, onde padrões modernos ajudam a abstrair a complexidade das chamadas de sistema, ou no processamento de **CPU**, onde estratégias como o modelo de atores podem ser aplicadas, o uso de bibliotecas externas é frequentemente desejável ao trabalharmos com C.

***

Introdução feita, vamos agora aprender a explorar recursos de concorrência em C!


# Forking de processos

Vamos começar com um exemplo simples que imprime "Hello" na saída padrão:

{% code title="forking.c" %}

```c
#include <stdio.h>

int main() {
	printf("Hello\n");
	return 0;
}
```

{% endcode %}

Primeiro passo é compilar o arquivo para um executável do SO:

```
$ gcc forking.c -o forking
```

Após isso, rodamos o executável e:

```
$ ./forking
Hello
```

> Dica: se quiser executar tudo em uma linha apenas, pode-se utilizar "gcc forking.c -o forking && ./forking"

*So far, so good.*

## O primeiro fork

Para fazer forking de processos em C, precisamos usar a syscall **fork** através do cabeçalho `unistd.h` (UNIX Standard), que fornece acesso a syscalls do Kernel, permitindo interagir diretamente com o sistema operacional:

{% code title="forking.c" %}

```c
#include <stdio.h>
#include <unistd.h>

int main() {
	fork();
	printf("Hello\n");
	return 0;
}
```

{% endcode %}

```
Hello
Hello
```

E por quê vemos agora a mensagem sendo impressa duas vezes? O quê acontece quando chamamos **fork**? Vamos detalhar o fluxo do programa:

* o processo pai é iniciado no começo do programa; apenas o pai está em execução
* quando o `fork` é chamado, o sistema cria um novo processo filho, que é *uma cópia exata do processo pai,* incluindo o estado do programa naquele exato momento
* qualquer código depois do fork é executado tanto no processo pai quanto no processo filho; como ambos chamam a função "printf" com "Hello", então a mensagem é impressa 2 vezes

Um ponto a destacar aqui é que, no momento do fork, todos os file descriptors do pai (incluindo o **STDOUT**) foram herdados pelo filho, e é por isso que vemos a mensagem na mesma saída padrão (tela do terminal) 2 vezes.

> É possível fazer com que o filho tenha outra saída padrão diferente do pai, mas não vamos entrar nestes detalhes aqui, não importam muito para o assunto principal que é concorrência

Se quisermos que cada um imprima uma mensagem diferente, teríamos que saber , de alguma forma **após a chamada do fork**, se estamos dentro do processo pai ou do filho.

## Manual, o nosso melhor amigo

De acordo com o [manual](https://man7.org/linux/man-pages/man2/fork.2.html), a  syscall fork tem o seguinte retorno:

> ```
> On success, the PID of the child process is returned in the
>        parent, and 0 is returned in the child.  On failure, -1 is
>        returned in the parent, no child process is created, and  is
>        set to indicate the error.
> ```

Ou seja, dentro do filho, o retorno da função é `0` . E dentro do pai, o retorno é o PID do filho. Com isto, podemos ter o seguinte código:

{% code title="forking.c" lineNumbers="true" %}

```c
#include <stdio.h>
#include <unistd.h>

int main() {
	pid_t pid = fork();
	if (pid == 0) {
		printf("Hello from child\n");
	} else {
		printf("Hello from parent\n");
	}
	return 0;
}
```

{% endcode %}

Que se executado, tem a seguinte saída:

```
Hello from parent
Hello from child
```

*Yay!*

Mas repare que, devido à natureza preemptiva do escalonador, não temos controle sobre qual será executado primeiro. Poderia ser o pai, ou mesmo o filho.&#x20;

O que temos que entender aqui é que, **no momento do fork**, é criado um *novo processo* no sistema operacional, que irá competir por recursos junto a outros processos,  ou seja, todo mundo junto no mesmo balaio de concorrência.

## Um exemplo mais robusto

Agora, vamos a um exemplo um pouco mais robusto com o uso de fork de processos, onde um processo pai dispara 3 processos filhos que irão executar uma tarefa que demora 2 segundos cada:

{% code title="forking-complex.c" lineNumbers="true" %}

```c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

void perform() {
	printf("Processo filho (PID: %d) executando tarefa...\n", getpid());
	sleep(2); // Simula uma tarefa que leva 2 segundos
	printf("Processo filho (PID: %d) completou a tarefa!\n", getpid());
}

int main() {
	int num_children = 3;
	pid_t pid;

	for (int i = 0; i < num_children; i++) {
		pid = fork(); // Cria um novo processo

		if (pid == 0) {
			// Processo filho
			perform();
			return 0;
		}
	}

	for (int i = 0; i < num_children; i++) {
		pid_t child_pid = wait(NULL); // Aguarda qualquer filho terminar
		printf("Pai: Processo filho com PID %d terminou.\n", child_pid);
	}

	printf("Pai: Todos os filhos terminaram. Finalizando.\n");
	return 0;
}
```

{% endcode %}

{% code lineNumbers="true" %}

```
Processo filho (PID: 271052) executando tarefa...
Processo filho (PID: 271053) executando tarefa...
Processo filho (PID: 271054) executando tarefa...
Processo filho (PID: 271052) completou a tarefa!
Processo filho (PID: 271053) completou a tarefa!
Pai: Processo filho com PID 271052 terminou.
Pai: Processo filho com PID 271053 terminou.
Processo filho (PID: 271054) completou a tarefa!
Pai: Processo filho com PID 271054 terminou.
Pai: Todos os filhos terminaram. Finalizando.
```

{% endcode %}

Interessante notar aqui:

* os 3 processos filhos foram criados e iniciaram sua execução (271052, 271053, 271054)
* 2 processos terminaram um pouco antes, sendo que o processo pai notificou que ambos tinham concluído execução
* um último processo (271054) terminou depois que o pai tinha notificado sobre os outros dois que concluíram primeiro
* o pai informa que todos os filhos foram concluídos e o programa finaliza

Isto, *senhoras e senhores*, é a maravilha da concorrência. Não temos controle algum sobre a ordem e execução das tarefas!

> Sim, vou repetir isso inúmeras vezes neste guia kk

## Comunicação entre processos (IPC)

Como processos não compartilham memória (a priori), precisamos de um mecanismo de comunicação entre processos, também chamado de **IPC** (Inter-process communication), pois um processo sem comunicação é bastante inútil.&#x20;

Uma das formas de IPC é através de **pipes,** que são uma comunicação *uni-direcional* entre processos.

> Se você quer entender mais sobre IPC e UNIX pipes, escrevi [um artigo sobre o tema](https://dev.to/leandronsp/entendendo-unix-pipes-3k56), onde é possível aprofundar nos conceitos de forma prática, e você só precisa de um terminal com shell/bash ou outra shell de preferência

```c
#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main() {
    int pipe_fd[2];
    pid_t pid;

    pid = fork();

    if (pid == 0) {
        // Processo filho
        close(pipe_fd[0]); // Fecha a extremidade de leitura
        char mensagem[] = "Message from child!";
        // Escreve no pipe
        write(pipe_fd[1], mensagem, strlen(mensagem) + 1);
        close(pipe_fd[1]); // Fecha a extremidade de escrita
    } else {
        // Processo pai
        close(pipe_fd[1]); // Fecha a extremidade de escrita
        char buffer[100];
        // Lê a partir do pipe
        read(pipe_fd[0], buffer, sizeof(buffer));
        printf("Parent received message: %s\n", buffer);
        close(pipe_fd[0]); // Fecha a extremidade de leitura
    }

    return 0;
}
```

Este é um exemplo bastante simples de como 2 processos distintos podem conversar entre si, através da utilização de UNIX pipes.

## Tá gostando do guia?

Se está gostando deste trabalho e considera fortalecer, o QRCode de PIX abaixo tá no jeito hein?

<figure><img src="/files/FUUkoZLpU19gvljXPbr1" alt="" width="283"><figcaption><p><br></p></figcaption></figure>

Ou copia a cola:

```
00020126850014BR.GOV.BCB.PIX013638ee4bde-574b-4197-b10f-68742087b00b0223Gratidão pelo cafezinho5204000053039865802BR5925Leandro Freitas Maringolo6009SAO PAULO62140510qrN6Ov1wRl63041A3C
```

***

Não vamos entrar em mais detalhes sobre uso de forking em C, isto já é o suficiente para o objetivo deste guia. No próximo tópico, iremos abordar o uso de **threads em C**.


# Threads

Agora que entendemos como funciona o forking de processos, vamos explorar as **threads**. Threads são uma forma mais leve de concorrência, permitindo que múltiplas tarefas sejam executadas dentro do mesmo processo, compartilhando o mesmo espaço de memória.

Conforme explicado na primeira parte do guia, no sistema operacional podemos criar threads através da chamada de sistema **clone** com os argumentos corretos. Na linguagem C, conseguimos manipular threads do sistema operacional, também chamadas de **kernel threads**.

Entretanto em C, embora podemos, não precisamos chamar a syscall **clone** diretamente devido à sua complexidade inerente. Mas para nossa sorte, na biblioteca padrão temos acesso a um cabeçalho chamado **pthread.h**, que abstrai de forma muito mais simples a criação de kernel threads utilizando [POSIX Threads](https://en.wikipedia.org/wiki/Pthreads) (ou *pthreads*), que são o padrão em sistemas UNIX-like:

<figure><img src="/files/Mtv69zCfLtmU3hp16kTV" alt="" width="563"><figcaption></figcaption></figure>

{% code title="pthreads.c" %}

```c
#include <stdio.h>
#include <pthread.h>

void* handle() {
	printf("Hello from thread!\n");
	return NULL;
}

int main() {
	pthread_t thread;

	// Cria uma thread
	pthread_create(&thread, NULL, handle, NULL);

	printf("Hello from main thread!\n");
	return 0;
}
```

{% endcode %}

A função *pthread\_create* recebe:

* um ponteiro para a variável que irá referenciar a thread na memória (**pthread\_t**)
* um ponteiro para a função que será executada no contexto da thread
* outros argumentos opcionais, que iremos deixar como NULL

Ao executar por algumas vezes o programa, podemos perceber a inconsistência nas mensagens, muitas vezes imprimindo apenas:

```
Hello from main thread
```

Bom, se você leu direitinho a primeira parte do guia, vai se lembrar que:

> Todo programa tem uma thread principal

Ou seja, quando o processo é iniciado, ele é encapsulado dentro de uma thread chamada "principal", pelo que quando falamos do processo em si, estamos também falando desta thread principal.

Entretanto, podemos ver que a mensagem da thread criada com `pthread_create` não apareceu na saída, e isto se deve à natureza de concorrência do escalonamento de tarefas do sistema operacional, onde verificamos em ação no tópico anterior com forking de processos.

Se eu rodar o programa, pode ser que a thread foi escalonada rapidamente e a mensagem aparece com sucesso. Mas pode ser também que a thread ainda não foi escalonada e o programa principal já foi finalizado. Enfim:&#x20;

> Não temos controle algum sobre a ordem e execução das tarefas no sistema operacional!

Como fazer com que o programa principal "espere" uma ou mais threads em execução serem finalizadas?

## Thread Join

Com a função `pthread_join` , o contexto da thread será trazido para o mesmo contexto da thread principal, então na prática o programa irá esperar pela execução da thread até que ela seja finalizada:

{% code title="pthreads.c" %}

```c
#include <stdio.h>
#include <pthread.h>

void* handle() {
	printf("Hello from thread!\n");
	return NULL;
}

int main() {
	pthread_t thread;

	// Cria uma thread
	pthread_create(&thread, NULL, handle, NULL);

	// Aguarda a thread terminar
	pthread_join(thread, NULL);

	printf("Hello from main thread!\n");
	return 0;
}
```

{% endcode %}

Saída:

```
Hello from thread!
Hello from main thread!
```

*Wow! How cool is that?*

## Um exemplo mais robusto

Já vimos os building blocks para criação de threads em C. Agora, vamos a um exemplo um pouco mais robusto, similar ao que aprendemos no tópico de forking de processos:

{% code title="pthreads-complex.c" lineNumbers="true" %}

```c
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void* handle(void* arg) {
	int thread_id = *((int*)arg); // Recebe o ID da thread
	printf("Thread %d is running...\n", thread_id);
	sleep(2); // Simula uma tarefa que leva 2 segundos
	printf("Thread %d is finished\n", thread_id);
	return NULL;
}

int main() {
	int num_threads = 3;
	pthread_t threads[num_threads];
	int thread_ids[num_threads];

	for (int i = 0; i < num_threads; i++) {
		thread_ids[i] = i + 1; // Identificação das threads
		pthread_create(&threads[i], NULL, handle, &thread_ids[i]);
	}

	for (int i = 0; i < num_threads; i++) {
		pthread_join(threads[i], NULL); // Aguarda cada thread terminar
		printf("Thread %d has been finished.\n", i + 1);
	}

	printf("All threads are finished.\n");
	return 0;
}
```

{% endcode %}

```
Thread 1 is running...
Thread 2 is running...
Thread 3 is running...
Thread 3 is finished
Thread 2 is finished
Thread 1 is finished
Thread 1 has been finished.
Thread 2 has been finished.
Thread 3 has been finished.
All threads are finished.
```

Repare que a ordem de execução pode mudar, devido à (e lá vamos novamente repetir) natureza da concorrência.

## Diferenças entre forking e threads

Até o momento, exploramos 2 formas de concorrência em C que são forking de processos e threads.&#x20;

No forking, a comunicação entre processos (IPC) precisa ser feita através de pipes ou forçar algum mecanismo de compartilhamento de memória, pois por padrão os processos não compartilham memória uns com os outros.

Ja com threads, cada thread compartilha o mesmo contexto que é a memória do **processo principal**, ou seja, duas ou mais threads podem provocar um cenário de condição de corrida se precisarem ler/escrever no mesmo recurso.

Para mitigar problemas de acesso a recurso compartilhado entre threads, precisamos recorrer ao uso de **locks**.


# Race condition e sincronização de threads com mutex

Para entendermos o problema que a *sincronização com mutex* resolve, vamos primeiro trazer o problema.&#x20;

A ideia é escrever um programa que cria 5 threads, onde cada thread faz um **incremento de 100 mil vezes** em uma variável (counter) compartilhada entre todas as threads.&#x20;

Ao fim do programa, queremos que o *counter* tenha um valor total de 500.000, ou seja, 5 threads x 100.000. Certo? Vamos então à implementação do programa:

```c
#include <stdio.h>
#include <pthread.h>

#define NUM_THREADS 5
#define INCREMENTS 100000

int counter = 0; // Variável compartilhada entre as threads

void* increment(void* arg) {
	for (int i = 0; i < INCREMENTS; i++) {
		counter++; // Incrementa a variável compartilhada
	}
	return NULL;
}

int main() {
	pthread_t threads[NUM_THREADS];

	// Cria as threads
	for (int i = 0; i < NUM_THREADS; i++) {
		pthread_create(&threads[i], NULL, increment, NULL);
	}

	// Aguarda as threads terminarem
	for (int i = 0; i < NUM_THREADS; i++) {
		pthread_join(threads[i], NULL);
	}

	printf("Valor final do counter compartilhado: %d (esperado: %d)\n", counter, NUM_THREADS * INCREMENTS);
	return 0;
}
```

Tudo ok até aqui, pois os comentários no código são auto-explicativos. Vamos executar o programa e:

```
Valor final do counter compartilhado: 282989 (esperado: 500000)
```

*Eita!* Note que o valor final do counter ficou bem abaixo do esperado. Experimente rodar outras vezes e repare que a cada execução, o valor final será diferente.&#x20;

Isto, *senhoras e senhores*, é a maravilha da concorrência. Não temos controle algum sobre a ordem e execução das tarefas!

> Sim, vou repetir isso inúmeras vezes neste guia kk

## Race condition

O que temos aqui é examente um cenário de **race condition**, ou *condição de corrida*, onde o valor final de um recurso compartlihado depende da ordem de execução das threads. Em outras palavras, este recurso precisa ser **sincronizado** entre as threads.&#x20;

E para isto, recorremos ao uso de travas - **locks** - que, para nossa sorte, a biblioteca padrão implementa uma abstração chamada **mutex** (exclusão mútua), através do uso da função `pthread_mutex_lock` .

## Mutex

O uso de mutex em C é muito simples. Tudo o que precisamos é de criar uma variável *compartilhada* que irá representar o mutex:

```c
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
```

E em volta da mutação do recurso, fazemos o bloqueio e desbloqueio do mutex:

```c
pthread_mutex_lock(&mutex);    // Bloqueia o mutex
counter++;                     // Incrementa a variável compartilhada
pthread_mutex_unlock(&mutex);  // Desbloqueia o mutex
```

O que vai acontecer, na prática, é que quando uma thread estiver com o mutex, e caso outra tente acessar o mesmo mutex, o sistema irá colocar esta outra thread em "wait" até que o mutex seja liberado (unlock).

<figure><img src="/files/gC6zp3hIfpHFRsk0Jb1p" alt="" width="563"><figcaption></figcaption></figure>

Agora vamos à implementação completa do código, sincronizado com mutex:

{% code title="race-condition-mutex.c" lineNumbers="true" %}

```c
#include <stdio.h>
#include <pthread.h>

#define NUM_THREADS 5
#define INCREMENTS 100000

int counter = 0; // Variável compartilhada entre as threads
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // Mutex para proteger o acesso

void* increment(void* arg) {
	for (int i = 0; i < INCREMENTS; i++) {
		pthread_mutex_lock(&mutex); // Bloqueia o mutex
		counter++; // Incrementa a variável compartilhada
		pthread_mutex_unlock(&mutex); // Desbloqueia o mutex
	}
	return NULL;
}

int main() {
	pthread_t threads[NUM_THREADS];

	// Cria as threads
	for (int i = 0; i < NUM_THREADS; i++) {
		pthread_create(&threads[i], NULL, increment, NULL);
	}

	// Aguarda as threads terminarem
	for (int i = 0; i < NUM_THREADS; i++) {
		pthread_join(threads[i], NULL);
	}

	printf("Valor final do counter compartilhado: %d (esperado: %d)\n", counter, NUM_THREADS * INCREMENTS);
	return 0;
}
```

{% endcode %}

```
Valor final do counter compartilhado: 500000 (esperado: 500000)
```

**Yay!** *Quantas palmas merece o mutex?*


# Desafios com o uso de threads

O uso de threads, apesar de ser mais leve que forking de processos, traz também alguns desafios. A criação da thread `pthread_create` tem um custo no sistema operacional, como podemos imaginar. O join também, e o `pthread_mutex_lock` e unlock não ficam de fora.

Todas essas chamadas de funções com relação ao uso de threads causam um overhead no sistema como um todo, faz o escalonador trabalhar mais através de múltiplas **trocas de contexto,** e isso contribui para o aumento da latência total do sistema. Essa troca de contexto é custosa, pois envolve salvar e restaurar o estado da thread (registradores, pilha, etc) e pode causar **cache misses** no processador.

O sistema operacional implementa diversas otimizações para gerenciar threads de forma eficiente, mas mesmo assim, o uso indiscriminado pode levar a problemas de desempenho e complexidade.

> Trabalhar com threads é complexo

## Quando usar threads com cautela?

Criar muitas threads em um sistema com recursos limitados pode causar degradação de desempenho devido ao aumento do overhead de gerenciamento das threads.

Por exemplo, em um sistema com 4 núcleos de CPU, criar **centenas de threads** geralmente não traz ganho adicional, pois a maioria delas ficará em espera devido a *sincronização com mutexes*. Múltiplas trocas de contexto irão piorar ainda mais o desempenho.

Para além de **race conditions**, podemos também enfrentar problemas muito difíceis como **deadlocks,** que são comuns em sistemas multithreaded e difíceis de depurar.

<figure><img src="/files/XqKWw2arnD5H5LLnwrvF" alt="" width="258"><figcaption><p>exemnplo de deadlock na vida real kkk</p></figcaption></figure>

> Em algumas situações, como por exemplo quando a T1 tem o acesso ao mutex, mas por algum motivo esse mutex ficou perdido na memória, então ela não consegue recuperar o mutex e portanto o recurso fica bloqueado para todas as outras threads, que estão à espera. Este efeito causa um **deadlock**, que lança um erro fatal no programa que o faz terminar imediatamente.

Por último, o sistema operacional determina uma **quantidade limitada de threads.** Isto por si só já nos limita bastante se precisarmos de disparar milhares de threads, como no caso de requisições HTTP em um web server. Este limite pode ser definido pelo utilitário `ulimit` em sistemas UNIX-like, e é configurado tanto a nível de usuário quanto de processo.

Todas estas limitações nos levam a:

* criar uma abstração de "pool" (ou piscina), onde múltiplas threads **previamente criadas** podem ser utilizadas e devolvidas ao pool, assim não precisamos criar milhares de threads indiscriminadamente;
* recorrer a alternativas "thread-safe", que garantem de alguma forma que a memória não é compartilhada entre as threads; ou
* implementar um mecanismo de "green threads", onde pequenas unidades de concorrência vivem dentro do runtime (implementação), e não necessariamente no sistema operacional. Isto requer a implementação de um escalonador para essas green threads

Vamos primeiramente abordar a implementação de uma **pool de threads** e como isto pode mitigar bastante os problemas inerentes ao uso de threads.


# Thread Pool em C

**Thread Pool** (ou "piscina de threads") é uma abstração que resolve os problemas associados ao uso indiscriminado de threads que vimos no tópico anterior. Em vez de criar e destruir threads para cada tarefa, uma pool de threads mantém um **conjunto fixo de threads**, previamente criadas, que executam tarefas conforme necessário.&#x20;

Assim, quando uma tarefa é concluída, a thread é *devolvida ao pool e reutilizada* para outras tarefas.

Isso mitiga problemas como overhead de criação de threads, limitação de recursos no sistema operacional e latência associada à troca de contexto e sincronização de threads.

## Funcionamento de uma thread pool

O funcionamento de uma pool de threads é muito simples, e passa por:

* criar um **número fixo** de threads
* cria uma **fila** de tarefas compartilhada entre as threads
* cada thread na pool:
  * retira uma tarefa da fila
  * executa a tarefa
  * **volta a ficar disponível** para a próxima tarefa

Em temos técnicos, cada thread *fica em loop* verificando se há tarefa nova na fila. Basicamente, o processo principal adiciona tarefas na fila (push), e depois cada uma das threads na pool fica retirando as tarefas da fila (pop):

<figure><img src="/files/niW9PcihGR0CyHTw4SyB" alt="" width="375"><figcaption></figcaption></figure>

Entretanto, o quê acontece se 2 ou mais threads tentarem fazer pop da fila ao mesmo tempo?

<figure><img src="/files/et846v6Z14NIfT2Zppjo" alt="" width="375"><figcaption></figcaption></figure>

Pode dar ruim, né? **Race condition**! E o quê fazemos pra resolver race condition?&#x20;

> Você acertou, tá mandando bem hein? Mutex neles!

<figure><img src="/files/WVMlZJOEORqBK9imSrg3" alt="" width="375"><figcaption></figcaption></figure>

Se a gente protege o recurso com um mutex, basicamente sincronizamos o acesso entre as threads e evitamos a condição de corrida. Contudo, o quê acontece se não houver nenhuma tarefa na fila?

<figure><img src="/files/cQxRM5pfRk4I1BPIHr1V" alt="" width="375"><figcaption></figcaption></figure>

Quando não há nada na fila, a operação de **pop** da fila retorna `NULL` , portanto a thread fica **repetindo o loop** até que uma nova tarefa seja adicionada na fila. Loops são CPU-intensive, portanto o uso da CPU ficaria bastante comprometido neste caso.

Mas a biblioteca padrão traz uma técnica primitiva para lidar com isso, que basicamente são **variáveis condicionais**, *condvar,* ou simplesmente **condition**.

Dada uma certa condição, a thread é colocada em estado de *wait*, e isto pausa a execução do loop da thread, evitando o consumo desnecessário de CPU. Este mecanismo usa por trás recursos de **thread signaling**, que basicamente é o envio de sinais para as threads.

<figure><img src="/files/dk4VrwISdlnyf7lkGZkE" alt="" width="375"><figcaption></figcaption></figure>

Assim que uma nova tarefa é adicionada na fila, é enviado o sinal de *wake* para a thread que detém o mutex, portanto esta ganha prioridade no escalonador e volta a executar o loop, fazendo **pop da tarefa da fila**.

<figure><img src="/files/cuqWIrNoR5neWsj4TrRq" alt="" width="375"><figcaption></figcaption></figure>

O uso de *condvar* deve ser feito em conjunto com **mutex**, pois a única forma de saber "qual thread acordar" é justamente a thread que detém o controle do mutex naquele exato momento.

## Mutex e condvar em C

Para nossa sorte, a biblioteca padrão traz suporte ao uso de condvars, através das funções `pthread_cond_wait` e `pthread_cond_signal` &#x20;

Agora, vamos abordar a implementação completa de uma thread pool em C, em pequenos passos. Primeiramente, incluímos os cabeçalhos necessários para o uso das funções do programa:

```c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
```

Definimos também as constantes do programa:

```c
#define NUM_THREADS 4    // Número de threads no pool
#define NUM_TASKS 10     // Número total de tarefas
```

A fila de tarefas será representada por uma simples struct, onde uma `Task` contém um `task_id` :

```c
// Fila de tarefas
typedef struct {
	int task_id;
} Task;

Task queue[NUM_TASKS];

// Contador e índice de tarefas
int task_count = 0;
int task_index = 0;
```

Agora, definimos as variáveis de sincronização, no caso o mutex e condvar:

```c
// Variáveis de sincronização
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t condvar = PTHREAD_COND_INITIALIZER;
```

Okay, próximo passo é a implementação da função `handle` , que será executada dentro de cada thread:

```c
// Função executada pelas threads do pool
void* handle(void* arg) {
	int thread_id = *((int*)arg);

	while (1) {
		// Bloqueia o mutex para acessar a fila de tarefas
		pthread_mutex_lock(&mutex);

		// Espera até que uma tarefa esteja disponível
		while (task_index >= task_count) {
			pthread_cond_wait(&condvar, &mutex);
		}

		// Retira a próxima tarefa da fila
		Task task = queue[task_index];
		task_index++;

		// Executa a tarefa
		printf("Thread %d processando tarefa %d...\n", thread_id, task.task_id);
		sleep(1); // Simula o processamento da tarefa
		printf("Thread %d completou tarefa %d.\n", thread_id, task.task_id);
		
		pthread_mutex_unlock(&mutex); // Desbloqueia o mutex		
	}

	return NULL;
}
```

Quando a função handle for executada:

* o ID da thread é enviado nos argumentos
* inicia-se um **loop infinito**. Então dentro do loop:
  * bloqueia o mutex
  * verifica se há tarefas na fila. Se houver:
    * retira a tarefa da fila
    * executa a tarefa
    * desbloqueia o mutex
    * volta ao início do loop
  * Caso não há tarefas na fila (fila vazia):
    * envia um sinal condicional de **wait** para a thread

*Simples, não?*

Próximo passo é implementar a função que *adiciona a tarefa na fila*, a função **add\_task**:

```c
// Adiciona uma tarefa à fila
void add_task(int task_id) {
	pthread_mutex_lock(&mutex);

	if (task_count < NUM_TASKS) {
		queue[task_count].task_id = task_id;
		task_count++;
		pthread_cond_signal(&condvar); // Notifica as threads
	} else {
		printf("Fila de tarefas cheia! Não foi possível adicionar tarefa %d.\n", task_id);
	}

	pthread_mutex_unlock(&mutex);
}
```

Repare que, depois de adicionar, é chamada a função `pthread_cond_signal` que envia um sinal de *wake* para a thread que detém o mutex. Isto faz com que a thread "acorde" e continue e execução depois do ponto do `pthread_cond_wait` localizado na função **handle** do exemplo anterior.

Por último, nos resta implementar a função **main**, que deve ser bastante simples:

* cria as 4 threads na pool
  * cada thread irá iniciar um loop infinito e ficar em estado de *wait* até que a primeira tarefa seja adicionada na fila (como explicado anteriormente)
* adiciona tarefas à fila, simulando um intervalo de tempo entre cada inserção de tarefa

{% code lineNumbers="true" %}

```c
int main() {
	pthread_t threads[NUM_THREADS];

	// Cria as threads no pool
	for (int i = 0; i < NUM_THREADS; i++) {
		int* thread_id = malloc(sizeof(int));
		*thread_id = i + 1;
		pthread_create(&threads[i], NULL, handle, thread_id);
	}

	// Adiciona tarefas à fila
	for (int i = 0; i < NUM_TASKS; i++) {
		printf("Adicionando tarefa %d\n", i + 1);
		add_task(i + 1);
		sleep(0.5); // Simula um intervalo entre tarefas
	}

	// Em um sistema real, seria necessário um mecanismo para finalizar as threads.
	// Aqui, como as threads ficam em loop infinito, use Ctrl+C para encerrar.
	for (int i = 0; i < NUM_THREADS; i++) {
		pthread_join(threads[i], NULL);
	}

	return 0;
}
```

{% endcode %}

**Atenção** para o último bloco entre as linhas 20 e 22: é preciso fazer o join das threads, entretanto as threads estão em **loop infinito**, portanto o programa *nunca irá terminar*, a não ser que seja utilizado o `Ctrl+C` .

## A implementação final

Agora sim, vamos colar tudo e mostrar para a galera no churrasco:

{% code title="thread-pool.c" lineNumbers="true" %}

```c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

#define NUM_THREADS 4    // Número de threads no pool
#define NUM_TASKS 10     // Número total de tarefas

// Fila de tarefas
typedef struct {
	int task_id;
} Task;

Task queue[NUM_TASKS];
int task_count = 0;
int task_index = 0;

// Variáveis de sincronização
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t condvar = PTHREAD_COND_INITIALIZER;

// Função executada pelas threads do pool
void* handle(void* arg) {
	int thread_id = *((int*)arg);
	free(arg); // Libera a memória alocada para o ID da thread

	while (1) {
		// Bloqueia o mutex para acessar a fila de tarefas
		pthread_mutex_lock(&mutex);

		// Espera até que uma tarefa esteja disponível
		while (task_index >= task_count) {
			pthread_cond_wait(&condvar, &mutex);
		}

		// Retira a próxima tarefa da fila
		Task task = queue[task_index];
		task_index++;

		// Executa a tarefa
		printf("Thread %d processando tarefa %d...\n", thread_id, task.task_id);
		sleep(1); // Simula o processamento da tarefa
		printf("Thread %d completou tarefa %d.\n", thread_id, task.task_id);

		pthread_mutex_unlock(&mutex); // Desbloqueia o mutex
	}

	return NULL;
}

// Adiciona uma tarefa à fila
void add_task(int task_id) {
	pthread_mutex_lock(&mutex);

	if (task_count < NUM_TASKS) {
		queue[task_count].task_id = task_id;
		task_count++;
		pthread_cond_signal(&condvar); // Notifica as threads
	} else {
		printf("Fila de tarefas cheia! Não foi possível adicionar tarefa %d.\n", task_id);
	}

	pthread_mutex_unlock(&mutex);
}

int main() {
	pthread_t threads[NUM_THREADS];

	// Cria as threads no pool
	for (int i = 0; i < NUM_THREADS; i++) {
		int* thread_id = malloc(sizeof(int));
		*thread_id = i + 1;
		pthread_create(&threads[i], NULL, handle, thread_id);
	}

	// Adiciona tarefas à fila
	for (int i = 0; i < NUM_TASKS; i++) {
		printf("Adicionando tarefa %d\n", i + 1);
		add_task(i + 1);
		sleep(0.5); // Simula um intervalo entre tarefas
	}

	// Em um sistema real, seria necessário um mecanismo para finalizar as threads.
	// Aqui, como as threads ficam em loop infinito, use Ctrl+C para encerrar.
	for (int i = 0; i < NUM_THREADS; i++) {
		pthread_join(threads[i], NULL);
	}

	return 0;
}
```

{% endcode %}

```
...

Thread 3 processando tarefa 3...
Adicionando tarefa 4
Thread 3 completou tarefa 3.
Thread 4 processando tarefa 4...
Adicionando tarefa 5
Thread 4 completou tarefa 4.
Thread 2 processando tarefa 5...
Adicionando tarefa 6
Thread 2 completou tarefa 5.
Thread 1 processando tarefa 6...
Adicionando tarefa 7
Thread 1 completou tarefa 6.
Thread 3 processando tarefa 7...
Adicionando tarefa 8
Thread 3 completou tarefa 7.
Thread 4 processando tarefa 8...
Adicionando tarefa 9

...
```

*Simplesmente maravilhoso, não?*


# Green threads

Quando falamos de **threads**, geralmente estamos nos referindo a *threads do sistema operacional*, ou **kernel threads**. Algumas implementações de linguagens de programação, como o *GCC em C*, permitem utilizar kernel threads, que no caso do C é através da função `pthread_create` .

Mas como vimos no tópico sobre os desafios com o uso de kernel threads, a gestão de milhares de threads pode aumentar consideravelmente a latência do sistema.

Como alternativa, algumas implementações optam por usar abstrações similar a threads dentro do próprio runtime, pelo que precisam desenvolver o próprio escalonador de threads.&#x20;

A essas threads que vivem dentro do runtime, ou como costumamos dizer, *a nível do usuário*, damos o nome de **green threads**.

Mas antes de falarmos das green threads, vamos relembrar o funcionamento de uma kernel thread.

## Funcionamento de uma kernel thread

Como já aprendemos nos tópicos anteriores, em C conseguimos criar threads do sistema operacional, ou kernel threads. Basicamente, uma kernel thread tem uma pilha onde guarda o estado de execução entre outros metadados:

<figure><img src="/files/DaoxIFp224I8OYOMPhWt" alt="" width="563"><figcaption></figcaption></figure>

Em C, a chamada da função `pthread_create` basicamente cria uma kernel thread, então a gestão de escalonamento e troca de contexto fica completamente a cargo do kernel (SO), e não do GCC:

<figure><img src="/files/Mtv69zCfLtmU3hp16kTV" alt="" width="563"><figcaption></figcaption></figure>

## Como funciona uma green thread

Uma green thread geralmente tem uma estrutura similar a uma kernel thread, contendo sua própria pilha de memória mas compartilhando a memória principal do processo.&#x20;

Mas uma green thread tem a vantagem de não levar com a latência da criação de kernel threads, ou seja, dependendo da *implementação de green threads no runtime*, podemos criar milhares, senão **milhões de green threads** mantendo baixa latência.

A nível de implementação, em determinado momento vamos precisar "associar" as green threads às kernel threads, afinal:

> Todo programa roda em uma thread principal, ou seja, para o escalonador do sistema operacional é *tudo thread*, ou melhor ainda, **é tudo task**

O desafio é então *multiplexar* um número arbitrário de green threads para kernel threads, e isto pode ser feito de várias formas. Vamos a seguir detalhar alguns tipos comuns de implementação de green threads.

### 1. Uma kernel thread, múltiplas green threads

Neste tipo de implementação, temos apenas uma thread principal do programa que é mapeada diretamente para uma kernel thread. E dentro desta thread, criamos múltiplas green threads:

<figure><img src="/files/VItrrvCskqNkwGpCOJio" alt="" width="563"><figcaption></figcaption></figure>

A vantagem desta abordagem é que temos **menos latência** comparando com o uso indiscriminado de kernel threads, mas por outro lado, a desvantagem é que **não temos um paralelismo real** mesmo em CPU multi-core, pois o sistema operacional não sabe que se trata de uma green thread, então no fim das contas é apenas **uma thread** sendo utilizada na CPU.

### 2. Multiplexação de N green threads para M kernel threads

Esta abordagem é muito comum no runtime Go, e é basicamente a capacidade de criar uma kernel thread para um conjunto específico de green threads:

<figure><img src="/files/n3gdAYDvuxGSBPbgBheP" alt="" width="563"><figcaption></figcaption></figure>

A vantagem nisso é que podemos utilizar mais paralelismo, uma vez que cada kernel thread pode utilizar um núcleo de CPU. Se o runtime for espertinho o suficiente, temos uma situação muito interessante para lidar com concorrência e paralelismo.

> Alô Gophers, o momento de vocês está chegando :P

### 3. Múltiplos escalonadores

Outra abordagem muito interessante, e que é utilizada na implementação do Erlang/Elixir (BEAM), é que poderíamos multiplexar kernel threads pra cada escalonador. Assim, cada escalonador iria cuidar de um número X de *green threads* mantendo o paralelismo real.

<figure><img src="/files/Rpfb0udECxuROvEwumaG" alt=""><figcaption></figcaption></figure>

Mas a ideia por enquanto não é falar de Go nem Erlang, pois estamos ainda no C. Chegou o momento de entender como podemos trabalhar com green threads em C.

## Green threads em C

Infelizmente, a implementação padrão do GCC não traz suporte a green threads. O que até faz sentido, imagina o C trazer isso? Seria overkill, sendo uma linguagem bastante genérica, de propósito geral e muito perto do sistema operacional.

Mas há algumas bibliotecas externas que trazem este conceito, onde podemos ter "green threads" em C contando com uma estrutura de escalonamento das threads:

* [libdill.org](https://libdill.org/): green threads (chamadas na lib de **corrotinas**), com comunicação baseada em canais e escalonamento cooperativo
* [libmill.org](https://libmill.org/): similar à libdill, mas implementada de forma mais robusta e escalável, com multiplexing de kernel threads semelhante ao que temos em Go
* ...entre outras, como libtask, libcoro, GNU pth etc


# Modelo de Atores

Outra forma de trabalhar com kernel threads que seja *thread-safe*, ou seja, utilizando threads de modo que não tenhamos problemas com condições de corrida, é através da implementação de **modelo de atores.**

Em tópicos anteriores vimos que a diferença entre processo e thread é que **processos não compartilham memória**, ao passo que **threads compartiham memória**. Isto por si só torna a utilização de processos segura em termos de race condition, mas com a desvantagem de utilizar bastante memória.&#x20;

E se quisermos utilizar threads sem compartilhar memória, é possível? Sim, através da técnica de **modelo de atores**.

## O quê é um ator?

Um ator, no contexto de concorrência, é uma "thread" (ou unidade de concorrência) que tem algumas propriedades muito parecidas com um processo do sistema operacional:

* não compartilha memória, ou seja, um ator tem seu próprio **estado privado**
* **possui identificação única** no sistema
* se comunica com outros atores através do **envio de mensagens**

As mensagens são enviadas para estruturas de fila, que usam aterminologia "inbox", ou **caixa de entrada**. Cada ator tem sua própria **inbox** para receber as mensagens.

<figure><img src="/files/AaQTARJmJ2oMM2LiyxHy" alt="" width="563"><figcaption></figcaption></figure>

Sendo assim, ao trabalhar com atores, não precisamos nos preocupar com uso de mutexes nem outro tipo de sincronização, pois somente o ator pode alterar seu próprio estado.

## Trabalhando com atores em C

Para o nosso azar, o GCC não traz uma implementação padrão ou ferramental para trabalharmos com modelo de atores. Mas algumas bibliotecas externas implementam a técnica:

* [libactor](https://github.com/airplug/libactor): uma implementação de modelo de atores que utiliza **pthreads**
* [czmq/zactor](https://github.com/zeromq/czmq?tab=readme-ov-file#zactor---simple-actor-framework): implementação de ator utilizando os building blocks do ZeroMQ


# Trabalhando com I/O

Relembrando o que vimos na primeira parte do guia, uma operação de I/O pode bloquear a execução de uma thread no sistema operacional. Sendo a *thread principal*, neste caso o **programa todo fica bloqueado**.

Para ilustrar como algumas operações de I/O são por natureza  bloqueantes, vamos demonstrar a comunicação entre dois processos utilizando **UNIX named pipes**, ou *FIFO* (veja mais sobre arquivos *FIFO* [no meu artigo](https://dev.to/leandronsp/implementando-um-simples-background-job-com-unix-named-pipes-3eja)).

## Comunicação com FIFO

O nome técnico para este tipo de comunicação é **UNIX named pipes,** ou *pipes nomeados*, cuja funcionalidade é prover uma estrutura de *fila* (daí o nome FIFO) onde:

* um processo escreve mensagens na fila
* outro processo lê mensagens na fila

A primeira coisa que temos que fazer é criar um arquivo especial do tipo "pipe" com o comando `mkfifo` :&#x20;

```bash
$ mkfifo queue

$ ls -l queue
prw-r--r-- 1 leandronsp leandronsp 0 Dec 31 23:59 queue
```

A saída começando com "p" indica justamente que este arquivo é especial, ou seja, um pipe FIFO.&#x20;

### Reader

Vamos representar em C o **leitor** da fila:

{% code title="fifo-reader.c" lineNumbers="true" %}

```c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

#define BUFFER_SIZE 1024

int main() {
    char buffer[BUFFER_SIZE];

    int fd = open("queue", O_RDONLY); // Abre o arquivo em modo somente leitura
    read(fd, buffer, BUFFER_SIZE); // Tentativa de leitura do arquivo
    printf("Mensagem recebida: %s", buffer);

    close(fd);
    return 0;
}
```

{% endcode %}

Atenção para alguns detalhes:

* o header **fcntl** inclui funções como `open` , que adiciona um determinado arquivo no filesystem na tabela de descritores de arquivos, a *file descriptor table*, e retorna o inteiro representando o **fd**&#x20;
* o header **unistd** inclui funções como `read` e `close` , que são mais genéricas para leitura e fechamento de descritores de arquivos&#x20;

Ao executar o programa, note que ele fica **bloqueado** a espera que alguém escreva na fila.

### Writer

Em *outra janela do terminal*, podemos escrever no FIFO utilizando o comando `echo` : &#x20;

```
$ echo Hello! > queue
```

Veja na janela do *writer* que a mensagem apareceu na saída padrão! *Superb!*&#x20;

Outra característica importante pra notar aqui: o writer também fica bloqueado de escrever, caso não haja nenhum reader disponível.

> Isto é o puro suco do I/O bloqueante

## Limitações do I/O bloqueante

Neste exemplo com FIFO, a operação de abertura do arquivo com `open` é **bloqueante.** Enquanto não chega mensagem no arquivo, o **programa fica bloqueado** nesta operação pois o I/O ainda *não está pronto***.**&#x20;

<figure><img src="/files/iVncKNmzGNZg4EnazJHu" alt=""><figcaption></figcaption></figure>

E é isto que define uma operação *bloqueante:* **quando o I/O ainda não está pronto***,* a nível de estruturas internas, dados disponíveis e buffers no sistema operacional.

Agora vamos imaginar um cenário onde precisamos ler do FIFO, depois ler do STDIN, ou mesmo ler de um socket *TCP,* e mais um **monte de coisas** no programa.

Bloquear o programa inteiro ou a thread inteira por conta de uma operação faz aumentar bastante a latência total do sistema, o que contribui para um throughput final bem baixo. Em outras palavras, nosso sistema não escala adequadamente consoante ao número de operações necessárias pra fazer em I/O.

<figure><img src="/files/lriiTqqw3p7wyjJPQR1e" alt="" width="270"><figcaption></figcaption></figure>

## Vai uma thread aí?

Não é incomum a utilização de threads para paralelizar operações em *I/O bloqueante* o que até faz sentido em alguns casos. Mas quando falamos de *milhares de operações*, podemos esbarrar nas limitações de threads, como já vimos diversas vezes neste guia.

> Sem problemas, Leandro. Já sabemos que dá pra usar pool de threads, certo?

*Certo*?

Mais ou menos. Embora seja uma solução que funcione para *muitos casos*, a pool pode limitar a performance caso o sistema exija um número massivo de operações em I/O, pois ela trabalha com um número fixo de threads, lembra?

> Pensando aqui, seria muito bacana se o sistema operacional permitisse que a leitura de um arquivo fosse "não-bloqueante", de forma que depois ele "avisasse" o programa que o arquivo já ficou pronto para leitura ou algo assim?

Esta técnica existe no sistema operacional, e é chamada de **I/O não-bloqueante**.

## I/O não-bloqueante em C

Como vimos no tópico anterior, a função `open` abre o descritor em modo bloqueante por padrão:

```c
int fd = open("queue", O_RDONLY); // Bloqueante
read(fd, buffer, BUFFER_SIZE); // Bloqueia até haver dados no FIFO
```

Mas é possível passar também uma flag chamada `O_NONBLOCK` , que vai abrir o descritor em modo **não-bloqueante**, e isto serve pra qualquer tipo de descritor, seja um arquivo comum, socket, pipe (FIFO), etc.

<pre class="language-c"><code class="lang-c">int fd = open("queue", O_RDONLY | O_NONBLOCK); // Não-bloqueante
<strong>ssize_t bytes_read = read(fd, buffer, BUFFER_SIZE); // Não bloqueia a leitura
</strong></code></pre>

Desta forma, a operação de I/O não impede o programa de continuar executando caso os dados não estejam imediatamente disponíveis. Mas aqui tem o *pulo do gato*...

Como a leitura não é bloqueante, a função retorna imediatamente. E o retorno pode ter dois valores possíveis:

* os bytes a serem lidos, caso o I/O já esteja pronto
* `-1` , caso o I/O não esteja pronto. Sendo assim, para além do valor ser **-1**, a variável global **errno** também é configurada com o valor `EAGAIN` ou `EWOULDBLOCK` , que indicam que o I/O não está pronto e que é preciso fazer "polling" no descritor para saber quando fica pronto

```c
int fd = open("queue", O_RDONLY | O_NONBLOCK); // Não-bloqueante
ssize_t bytes_read = read(fd, buffer, BUFFER_SIZE);

if (bytes_read < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
    printf("Nenhum dado disponível no FIFO no momento. Tentar novamente em instantes");
}
```

E atenção para a frase "tentar novamente em instantes". O que queremos aqui é *ficar em loop*, verificando (polling) quando que de fato há bytes para serem lidos:

{% code title="fifo-nb.c" lineNumbers="true" %}

```c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

#define BUFFER_SIZE 1024

int main() {
    char buffer[BUFFER_SIZE];

    int fd = open("queue", O_RDONLY | O_NONBLOCK); // Abre o arquivo em modo não-bloqueante
						   //
    while (1) { 
	ssize_t bytes_read = read(fd, buffer, BUFFER_SIZE);

	if (bytes_read > 0) {
	    printf("Mensagem recebida: %s", buffer);
	} else if (errno == EAGAIN || errno == EWOULDBLOCK) {
	    printf("Nenhum dado disponível no FIFO agora.\n");
	}

	sleep(1); // Espera 1 segundo
    }

    close(fd);
    return 0;
}
```

{% endcode %}

Dentro do loop, fazemos a leitura do descritor, e caso não esteja disponível, avisamos que não há nada na fila e a seguir é provocado um **sleep** de 1 segundo para não sobrecarregar a CPU com muitas voltas redundantes no loop.

```
Mensagem recebida: Hello
Mensagem recebida: World
```

*Yay!*

Contudo, esta solução ainda é bastante rudimentar e ineficiente para monitorarmos descritores de arquivos. Felizmente, o sistema operacional fornece algumas *syscalls* para resolver este problema de forma mais elegante, permitindo **monitorar os descritores** que estão sendo processados pelo sistema operacional.

## Monitorando I/O com select

A **syscall select** permite monitorar múltiplos descritores de arquivo simultaneamente, sem que o programa precise verificar continuamente cada um deles de forma redundante (como no exemplo anterior). Isso melhora a eficiência, reduz o consumo de CPU e permite que o programa reaja apenas quando houver atividade em um ou mais descritores monitorados.

Para utilizar o **select**, precisamos primeiro definir o conjunto de descritores (**fd\_set**) a serem monitorados, através das macros que estão na biblioteca *sys/select.h:*

* **FD\_ZERO:** para inicializar o conjunto
* **FD\_SET**: adiciona um descritor ao conjunto
* **FD\_ISSET**: verifica se um descritor está pronto para leitura/escrita

{% code title="io-select.c" lineNumbers="true" %}

```c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/select.h>

#define BUFFER_SIZE 1024

int main() {
    char buffer[BUFFER_SIZE];

    // Abre o FIFO em modo não-bloqueante
    int fd = open("queue", O_RDONLY | O_NONBLOCK);
    printf("Monitorando FIFO...\n");

    while (1) {
        fd_set read_fds;
        FD_ZERO(&read_fds);

        // Adiciona o FIFO aos conjuntos de leitura
        FD_SET(fd, &read_fds);

        // Configura timeout opcional (1 segundo)
        struct timeval timeout;
        timeout.tv_sec = 1;
        timeout.tv_usec = 0;

        // Chama select para monitorar os descritores
        int ready = select(fd + 1, &read_fds, NULL, NULL, &timeout);
        if (ready == 0) {
            // Timeout: Nenhuma atividade
            printf("Nenhum descritor disponível. Continuando...\n");
            continue;
        }

        // Verifica se há dados no FIFO, ou seja se está pronto para leitura
        if (FD_ISSET(fd, &read_fds)) {
            ssize_t bytes_read = read(fd, buffer, BUFFER_SIZE);

            if (bytes_read > 0) {
                printf("Mensagem recebida: %s", buffer);
            } 
        }
    }

    close(fd);
    return 0;
}

```

{% endcode %}

```
Monitorando FIFO...
Nenhum descritor disponível. Continuando...
Nenhum descritor disponível. Continuando...
Mensagem recebida: hello!
Mensagem recebida: hello!
```

*OMG!* Que dia incrível!

O *select* traz a vantagem que permite monitorar múltiplos descritores de forma simples, pelo que também funciona em diversos sistemas operacionais.

Entretanto ele possui algumas limitações:

* tempo de busca é linear, o que pode trazer problemas de performance para um grande número de descritores
* possui limitação no número máximo de descritores

## Monitorando I/O com epoll

**epoll** é uma interface mais eficiente que o *select* para monitorar um grande número de descritores, e foi introduzida no Linux 2.6.&#x20;

Em vez de retornar todos os descritores para fazer uma busca linear nos que estão prontos, *epoll* retorna **apenas os que estão de fato prontos** para leitura/escrita, diminuindo bastante o overhead para um número muito grande de descritores.

Outra vantagem com relação ao *select* é que não há um limite de descritores, pois ele mantém um número dinâmico de descritores dentro do Kernel.

A mecânica de uso é muito parecida com o select:

{% code title="epoll.c" lineNumbers="true" %}

```c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/epoll.h>

#define BUFFER_SIZE 1024
#define MAX_EVENTS 10

int main() {
    char buffer[BUFFER_SIZE];

    // Abre o FIFO em modo não-bloqueante
    int fd = open("queue", O_RDONLY | O_NONBLOCK);

    // Inicializa o epoll (similar ao FD_ZERO do select)
    int epoll_fd = epoll_create1(0);

    // Configura o descritor no epoll
    struct epoll_event event;
    event.events = EPOLLIN; // Monitorar para leitura
    event.data.fd = fd;
    // Adiciona o descritor ao epoll (similar ao FD_SET)
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event);

    printf("Monitorando FIFO com epoll...\n");

    struct epoll_event events[MAX_EVENTS];

    while (1) {
        // Aguarda pelos descritores prontos com timeout de 1 segundo
        int ready = epoll_wait(epoll_fd, events, MAX_EVENTS, 1000);

        if (ready == 0) {
            printf("Nenhum descritor disponível. Continuando...\n");
            continue;
        }

        // Itera pelos descritores prontos e realiza as devidas leituras
        for (int i = 0; i < ready; i++) {
            if (events[i].events & EPOLLIN) {
                ssize_t bytes_read = read(events[i].data.fd, buffer, BUFFER_SIZE);

                if (bytes_read > 0) {
                    printf("Mensagem recebida: %s\n", buffer);
                }
            }
        }
    }

    close(fd);
    close(epoll_fd);
    return 0;
}
```

{% endcode %}

A utilização de **epoll** apresenta uma performance significativamente superior ao **select** quando lidamos com um grande número de descritores de arquivo. Isso o torna uma escolha mais adequada para sistemas modernos e de alta escala.

***

Até agora, exploramos os recursos que o sistema operacional oferece para realizar operações de I/O de forma *não-bloqueante*. Essa característica muda fundamentalmente a maneira como escrevemos código, afastando-nos da abordagem tradicional síncrona para adotar uma abordagem assíncrona.

Normalmente, escrevemos código de forma síncrona, onde as funções e rotinas são executadas em sequência e os dados necessários já estão disponíveis na memória. Com I/O não-bloqueante, perdemos essa certeza. Não sabemos quando os dados estarão prontos, o que nos obriga a estruturar o código para lidar com a disponibilidade futura das informações.

Essa mudança nos leva ao **assincronismo**, onde não esperamos pela conclusão de uma operação, mas configuramos o código para reagir quando a operação for concluída.

## I/O assíncrono

Muitas vezes, há confusão entre I/O assíncrono e I/O não-bloqueante, ou mesmo com o uso de threads. No entanto, as threads não têm relação direta com o assincronismo em I/O. Podemos usar threads para multiplexar operações bloqueantes ou não-bloqueantes, mas o conceito de I/O assíncrono está relacionado à **maneira como o código é estruturado** para lidar com operações não-bloqueantes.

Em resumo, se você optar por I/O não-bloqueante, estará implicitamente adotando um estilo de programação **assíncrono** para lidar com a natureza imprevisível das operações.

Vamos ilustrar isso comparando um código síncrono com outro assíncrono em C:

```c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

#define BUFFER_SIZE 1024

int main() {
    char buffer[BUFFER_SIZE];

    // Abrindo um arquivo para leitura síncrona
    int fd = open("example.txt", O_RDONLY);

    // Lendo o conteúdo do arquivo
    ssize_t bytes_read = read(fd, buffer, BUFFER_SIZE);
    
    if (bytes_read > 0) {
        printf("Dados lidos: %s\n", buffer);
    } 

    close(fd);
    return 0;
}
```

O programa espera a leitura do arquivo ser concluída antes de continuar. A execução é **bloqueante**: o programa para no *read* até que a operação termine.

Já no código **assíncrono**, o programa continua a execução enquanto espera que a operação seja concluída. Quando os dados estão disponíveis, um **callback é acionado**. Um exemplo com o epoll:

```c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/epoll.h>

#define BUFFER_SIZE 1024
#define MAX_EVENTS 10

// Callback para tratar dados prontos para leitura
void on_data_available(int fd) {
    char buffer[BUFFER_SIZE];
    ssize_t bytes_read = read(fd, buffer, BUFFER_SIZE);
    
    if (bytes_read > 0) {
        printf("Callback: Dados lidos: %s\n", buffer);
    } 
}

int main() {
    // Abre o arquivo em modo não-bloqueante
    int fd = open("example.txt", O_RDONLY | O_NONBLOCK);

    // Cria o epoll
    int epoll_fd = epoll_create1(0);

    // Configura o descritor no epoll
    struct epoll_event event;
    event.events = EPOLLIN; // Monitorar para leitura
    event.data.fd = fd;

    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event)
    struct epoll_event events[MAX_EVENTS];
    
    // Loop de eventos do epoll
    while (1) {
        int ready = epoll_wait(epoll_fd, events, MAX_EVENTS, 1000);

        if (ready == 0) {
            printf("Nenhum dado disponível, continuando...\n");
            continue;
        }

        for (int i = 0; i < ready; i++) {
            if (events[i].events & EPOLLIN) {
                // Chama o callback para processar os dados
                on_data_available(events[i].data.fd);
            }
        }
    }

    close(fd);
    close(epoll_fd);
    return 0;
}
```

Vamos analisar passo-a-passo este exemplo assíncrono com epoll:

* criamos um **callback** `on_data_available` que representa a lógica quando o descritor estiver pronto (dados disponíveis)
* no código principal, temos:
  * a abertura do arquivo de forma **não-bloqueante**
  * configuração dos descritores com epoll
  * loop monitorando os eventos do epoll (loop de eventos)
    * dentro do loop, quando o dado fica disponível, chamamos o **callback** que foi registrado previamente

Isto, *senhoras e senhores*, é o puro extrato do assincronismo.

***

Agora vamos falar de uma limitação do *epoll*: só funciona no Linux. Outro desafio no uso do epoll também é que temos que configurar muita coisa manualmente e também temos de escrever nosso próprio "loop de eventos".

> E isso dá trabalho

## Libuv, o queridinho do NodeJS&#x20;

Não ganhamos na loteria mas estamos com muita sorte, pois a biblioteca [libuv](https://github.com/libuv/libuv) resolve estes e outros problemas:

* multi-plataforma (usa epoll no Linux, IOCP no Windows e kqueue no macOS)
* tem um loop de eventos muito bem feito
* traz suporte  uma pool de threads para algumas operações específicas de modo a aumentar o throughput
* permite tratamento de sinais
* entre otras cositas más...

Vamos a um exemplo de código assíncrono com libuv, a título de curiosidade:

{% code title="io-libuv.c" lineNumbers="true" %}

```c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <uv.h>

#define BUFFER_SIZE 1024

// Callback chamado quando a leitura é concluída
void on_data_available(uv_fs_t* req) {
    if (req->result > 0) {
        printf("Conteúdo do arquivo: %s\n", (char*)req->bufs->base);
    } 

    // Libera recursos
    uv_fs_req_cleanup(req);
    free(req->bufs->base);
    free(req);
}

int main() {
    // Inicializa o loop de eventos
    uv_loop_t* loop = uv_default_loop();

    uv_fs_t* open_req = malloc(sizeof(uv_fs_t));
    uv_fs_t* read_req = malloc(sizeof(uv_fs_t));

    // Abre o arquivo de forma assíncrona
    uv_fs_open(loop, open_req, "example.txt", O_RDONLY, 0, NULL);

    if (open_req->result >= 0) {
        char* buffer = malloc(BUFFER_SIZE);
        uv_buf_t iov = uv_buf_init(buffer, BUFFER_SIZE);

        // Lê o arquivo de forma assíncrona
        read_req->bufs = &iov;
        uv_fs_read(loop, read_req, open_req->result, &iov, 1, -1, on_data_available);
    }

    uv_fs_req_cleanup(open_req);
    free(open_req);

    uv_run(loop, UV_RUN_DEFAULT);
    return 0;
}
```

{% endcode %}

Repare como que o código fica mais simples, e não à toa o NodeJS utiliza o libuv como parte central de todo o assincronismo que o NodeJS fornece.

<figure><img src="/files/nSGn3XB7YreJT1k9A0Mz" alt="" width="563"><figcaption></figcaption></figure>

> Calma jovem, mais pra frente no guia vou dedicar uma seção só pra falar de concorrência em NodeJS

## Deixando seu código assíncrono, mas síncrono

Apesar do poder do assincronismo, a indireção que deixa no código dificulta muito a manutenção do sistema, porque lidamos com callbacks ou estruturas de eventos que fragmentam o fluxo lógico do programa. Callbacks e mais callbacks podem nos fazer entrar num cenário horrível na programação: o famigerado **callback hell**.

<figure><img src="/files/fJ5E1gilJNX5ylV97frA" alt=""><figcaption></figcaption></figure>

No entanto, podemos organizar o **código assíncrono de forma que pareça síncrono** aos olhos de quem está vendo o código, mesmo lidando com operações assíncronas internamente.

### I/O assíncrono e escalonamento cooperativo

Essa abordagem é frequentemente implementada usando **estruturas cooperativas** como *async/await, corrotinas ou geradores*. Elas pausam a execução no ponto de espera, liberando o controle do programa enquanto aguardam o evento (como dados de I/O) e retomam automaticamente quando o evento ocorre.

Usando algumas técnicas é possível fazer isto em C, mas não vamos aprofundar muito pois estas técnicas são implementadas de forma muito mais robusta em linguagens de mais alto nível que C.

***

E é isto que vamos cobrir nos tópicos a seguir, em como implementações de linguagens de alto nível lidam com concorrência, seja a nível de forking, threading, sincronização, abstrações em runtime e I/O assíncrono.

*Vamos continuar firmes*, a viagem ainda está no início :rocket:

## Está gostando deste trabalho?

Se está gostando e considera colaborar para que mais trabalhos assim sejam feitos, não deixe de entrar para a [lista de **investidores premium**](https://concorrencia101.leandronsp.com/agradecimentos). Qualquer apoio financeiro via PIX é mais que bem-vindo, inclusive compartilhar o guia é uma excelente forma de apoiar o projeto também!

<figure><img src="/files/FUUkoZLpU19gvljXPbr1" alt="" width="283"><figcaption></figcaption></figure>

Ou copia e cola:

```
00020126850014BR.GOV.BCB.PIX013638ee4bde-574b-4197-b10f-68742087b00b0223Gratidão pelo cafezinho5204000053039865802BR5925Leandro Freitas Maringolo6009SAO PAULO62140510qrN6Ov1wRl63041A3C
```

Obrigado :pray: E bora lá continuar na jornada!


# Concorrência em Ruby

> "Ruby não escala!"

Quem nunca ouviu essa frase antes? *Ela tem um fundo de verdade,* mas muita inconsistênci&#x61;*.* Neste módulo, vamos abordar todos os aspectos de concorrência em Ruby e tentar desmistificar algumas afirmações como "não dá pra paralelizar com Ruby" ou até mesmo "Ruby não tem multi-threading".

Estas afirmações costumam vir carregadas de desconhecimento. Ruby traz um suporte muito robusto a concorrência, inclusive nas versões modernas.

> Muito do que dizem "Ruby é lento" é na verdade com relação ao Ruby antigo 1.8, que vamos comentar um pouco mais a seguir

## Já viu o módulo "Concorrência em C" antes?

Se você chegou até aqui, acredito que já tenha lido o primeiro módulo deste capítulo II sobre *Concorrência em C*. Caso contrário, apesar de que seja altamente *recomendado* que faça primeiro o módulo de C (não é complexo, eu garanto), cada módulo sobre as linguagens de alto nível foi pensado para ser **independente**.

Portanto, se você já tem um bom conhecimento sobre concorrência no geral e todo o vocabulário envolvido mas tem interesse em apenas aprender como funciona **concorrência em Ruby,** não há problema se quiser ler apenas este módulo.

> Entretanto se você pular o módulo de C, pode ser que se sinta perdido(a) em alguns pontos, mas recomendo fortemente que leia antes a primeira parte do guia, sobre *Concorrência no Sistema Operacional*

***

## Preâmbulo

A implementação de Ruby mais amplamente utilizada é o interpretador **CRuby** (escrita em C), também conhecido popularmente como **MRI**, que vem de *Matz Ruby Interpreter.*

> Se você tem interesse em algo introdutório sobre a linguagem Ruby, eu escrevi [este artigo](https://dev.to/leandronsp/a-comprehensive-introduction-to-ruby-8o7) que aborda sua história, principais características e funcionalidades.

Os exemplos aqui trazidos funcionam na última versão estável do interpretador atualmente - Jan/2025 -, que é a `3.4.1` .

No que diz respeito à concorrência, [**Ruby 3.4.1**](https://www.ruby-lang.org/en/documentation/installation/) traz consigo na biblioteca padrão:

* Forking de processos
* Kernel Threads e sincronização de threads
* Modelo de atores (ainda experimental)
* Fibers (porém o escalonamento precisa ser implementado por nós)
* I/O não-bloqueante e monitoramento de descritores, através de `IO.select`&#x20;

E através de bibliotecas externas (gems), podemos ter tudo o que a biblioteca padrão já traz, inclusive:

* Implementações mais robustas de threading/forking
* Implementação de Thread Pool (embora não seja complexo implementar uma pool, como vimos em C, pelo que em Ruby é *flocos com morango*)
* I/O assíncrono, com loop de eventos, suporte a epoll etc

Vamos ver tudo isto em detalhes nos próximos tópicos.


# Forking de processos

Para aqueles que já leram a [parte I do guia](https://concorrencia101.leandronsp.com/parte-i-concorrencia-no-sistema-operacional/o-que-e-o-programa-no-sistema-operacional), não terão qualquer dificuldade em entender os conceitos aqui abordados. E caso também você tenha **real interesse** em aprender sobre concorrência, certamente já leu como funciona [forking de processos em C](https://concorrencia101.leandronsp.com/parte-ii-concorrencia-em-diferentes-linguagens/concorrencia-em-c/forking-de-processos).

Em Ruby, existe um método especial chamado `fork` que, na hora de ser executado, faz a chamada de sistema *fork.*

Vamos ver o primeiro exemplo similar ao que vimos inicialmente no módulo de C:

{% code title="forking.rb" %}

```ruby
fork
puts "Hello"
```

{% endcode %}

Agora vamos executar o programa utilizando o interpretador CRuby chamando `ruby forking.rb` , que imprime:

```
Hello
Hello
```

So far, so good. Já vimos anteriormente o motivo de aparecer `Hello` 2 vezes. Em um outro exemplo, podemos ver que o pid é diferente dependendo se estamos no parent ou no child:

```ruby
pid = fork

if pid
  puts "In parent, pid is #{Process.pid}. Child is #{pid}"
else
  puts "In child, pid is #{Process.pid}"
end
```

```
In parent, pid is 77278. Child is 77430
In child, pid is 77430
```

> Leiam a [documentação](https://ruby-doc.org/core-3.0.0/Process.html#method-c-pid), sempre

## Um exemplo mais robusto

Agora, vamos a um exemplo um pouco mais robusto com o uso de fork de processos, onde um processo pai dispara 3 processos filhos que irão executar uma tarefa que demora 2 segundos cada:

{% code title="forking.rb" lineNumbers="true" %}

```ruby
def perform
  pid = Process.pid

  puts "Processo filho (PID: #{pid}) executando tarefa..."
  sleep(2) # Simula uma tarefa que leva 2 segundos
  puts "Processo filho (PID: #{pid}) completou a tarefa!"
end

wait_pids = []

# Criação dos processos filhos
3.times do
  pid = fork do
    perform # Código do processo filho
  end

  wait_pids << pid # Armazena o PID do filho para controle
end

# O processo pai aguarda cada filho terminar
wait_pids.each do |child_pid|
  Process.wait(child_pid)

  puts "Pai: Processo filho com PID #{child_pid} terminou."
end

puts "Pai: Todos os filhos terminaram. Finalizando."
```

{% endcode %}

```
Processo filho (PID: 78317) executando tarefa...
Processo filho (PID: 78319) executando tarefa...
Processo filho (PID: 78318) executando tarefa...
Processo filho (PID: 78318) completou a tarefa!
Processo filho (PID: 78317) completou a tarefa!
Processo filho (PID: 78319) completou a tarefa!
Pai: Processo filho com PID 78317 terminou.
Pai: Processo filho com PID 78318 terminou.
Pai: Processo filho com PID 78319 terminou.
Pai: Todos os filhos terminaram. Finalizando.
```

Interessante notar aqui:

* os 3 processos filhos foram criados e iniciaram sua execução nesta ordem (78317, 78318, 78319)
* entretanto, de acordo com as mensagens, o processo 78318 terminou antes dos outros 2
* mas para o processo pai, o filho que terminou primeiro foi o 78317

Isto, *senhoras e senhores*, é a maravilha da concorrência. Não temos controle algum sobre a ordem e execução das tarefas!

> Sim, vou repetir isso inúmeras vezes neste guia kk

## Comunicação entre processos (IPC)

Assim como em C, podemos fazer **IPC** em Ruby utilizando [pipes](https://ruby-doc.org/core-3.0.0/IO.html#method-c-pipe):

```ruby
# Cria um pipe com dois descritores (leitura e escrita)
read_fd, write_fd = IO.pipe

pid = fork do
  # Processo filho
  read_fd.close # Fecha a extremidade de leitura no filho
  mensagem = "Message from child!"
  write_fd.puts(mensagem) # Escreve a mensagem no pipe
  write_fd.close # Fecha a extremidade de escrita no filho
end

# Processo pai
write_fd.close # Fecha a extremidade de escrita no pai
mensagem_recebida = read_fd.gets.chomp # Lê a mensagem do pipe
puts "Parent received message: #{mensagem_recebida}"
read_fd.close # Fecha a extremidade de leitura no pai

# Aguarda o processo filho terminar
Process.wait(pid)
```

Este é um exemplo bastante simples de como 2 processos distintos podem conversar entre si, através da utilização de UNIX pipes.

Repare como que até o momento, conseguimos fazer em Ruby tudo o que foi possível fazer em C com relação a **forking de processos**, e isto se deve ao fato de que o interpretador CRuby é escrito em C, o que torna simples a interface para as syscalls envolvidas no sistema operacional.

Agora, chegou o momento de vermos as *Threads* em Ruby.


# Threads

Agora que entendemos como funciona o forking de processos, vamos explorar as **threads**. Threads são uma forma mais leve de concorrência, permitindo que múltiplas tarefas sejam executadas dentro do mesmo processo, compartilhando o mesmo espaço de memória.

Conforme explicado no módulo de C, conseguimos criar POSIX Threads através da chamada de função `pthread_create`. Na linguagem Ruby, conseguimos manipular kernel threads [através da classe Thread.](https://ruby-doc.org/core-3.0.0/Thread.html)

<figure><img src="/files/mIbCMIxb7flXRlCbORAu" alt="" width="563"><figcaption></figcaption></figure>

```ruby
# Função que será executada pela thread
def handle
  puts "Hello from thread!"
end

# Cria uma thread
thread = Thread.new do
  handle
end

puts "Hello from main thread!"

# Aguarda a thread terminar
thread.join
```

```
Hello from main thread!
Hello from thread!
```

*Simples, não?* Podíamos também passar qualquer código Ruby pra dentro do bloco da thread, não precisa ser necessariamente um método:

```ruby
# Cria uma thread
thread = Thread.new do
  puts "Hello from thread!"
end

puts "Hello from main thread!"

# Aguarda a thread terminar
thread.join
```

## Um exemplo mais robusto

Agora vamos fazer um exemplo mais complexo com threads, assim como fizemos com forking de processos:

```ruby
def handle(thread_id)
  puts "Thread #{thread_id} is running..."
  sleep(2) # Simula uma tarefa que leva 2 segundos
  puts "Thread #{thread_id} is finished."
end

threads = []

# Criação das threads
3.times do |i|
  threads << Thread.new(i + 1) do |thread_id|
    handle(thread_id) # Passa o ID da thread como argumento
  end
end

# Aguarda todas as threads finalizarem
threads.each_with_index do |thread, i|
  thread.join
  puts "Thread #{i + 1} has been finished."
end

puts "All threads are finished."
```

```
Thread 1 is running...
Thread 2 is running...
Thread 3 is running...
Thread 1 is finished.
Thread 3 is finished.
Thread 2 is finished.
Thread 1 has been finished.
Thread 2 has been finished.
Thread 3 has been finished.
All threads are finished.
```

*Ruby dispensa comentários, não?*

***

Vamos agora ver como Ruby resolve problemas de **race condition.**


# Race condition, YARV, GVL e paralelismo em Ruby

Para entender o que de fato é **race condition**, sugiro fortemente a leitura deste mesmo tópico [no módulo de C](https://concorrencia101.leandronsp.com/parte-ii-concorrencia-em-diferentes-linguagens/concorrencia-em-c/race-condition-e-sincronizacao-de-threads-com-mutex).&#x20;

Com isto em mente, vamos mostrar um exemplo em Ruby onde múltiplas threads mudam o valor de uma **variável compartilhada,** que no final deveria mostrar um valor inconsistente:

```ruby
balance = 0 # Variável compartilhada

# Cria 100 threads
threads = 100.times.map do
  Thread.new do
    500_000.times do
      balance += 1
    end
  end
end

# Espera as 100 threads terminarem de executar
threads.each(&:join)

puts "Balance is: #{balance} (expected: 50000000)"
```

Vamos rodar o programa e:

```
Balance is: 50000000 (expected: 50000000)
```

> Ué? Não era pro saldo final ser diferente devido a race condition, conforme aprendemos no módulo anterior?

Estranho, bora executar de novo então:

```
Balance is: 50000000 (expected: 50000000)
```

Que bizarro...de novo:

```
Balance is: 50000000 (expected: 50000000)
```

Após executar umas 20 vezes, o resultado ainda é o mesmo.

> Tá brincando com a minha cara, né Leandro? Não há race condition em Ruby, é isso? Poderíamos dizer então que Ruby já tem um "mutex embutido" que previne race conditions?

*Quase isso*, mas não é isso. Já vamos explicar o que exatamente está acontecendo, mas antes vamos entender brevemente o que é esse tal "interpretador Ruby" (CRuby).

## Ruby VM, ou YARV

Voltando vários anos atrás, a implementação oficial do Ruby até a versão 1.8, era um *simples interpretador*. Ou seja, após transformar o código Ruby em uma árvore de sintaxe (AST), o interpretador executava diretamente cada nó da árvore chamando funções em C pré-compiladas. É o que chamamos de *interpretador AST-based*. Inclusive, nesta versão as threads não eram kernel threads, mas sim **green threads**.

Entretanto, haviam alguns problemas com este interpretador CRuby 1.8:

* era lento, pois cada comando percorria a AST do zero, sem otimizações
* as green threads eram cooperativas e rodavam todas na mesma thread do sistema operacional, o que impedia o uso de múltiplos núcleos de CPU
* o garbage collector (GC) era "stop-the-world" e pausava todo o programa durante a coleta

<figure><img src="/files/ncA0vqAVfBVmbeZTlquE" alt="" width="563"><figcaption></figcaption></figure>

Para resolver estes problemas, a **versão 1.9 trouxe diversas melhorias**. Foi feito um grande refactoring do interpretador, principalmente na parte de execução de código, onde foi implementada uma fina camada de **máquina virtual** (VM), também chamada carinhosamente pelos Rubystas de **YARV**, ou *Yet Another Ruby Virtual Machine*.

> Criativo, não? kk

Se quer entender um pouco mais sobre esse vocabulário de *interpretadores*, sugiro a [leitura deste meu artigo](https://dev.to/leandronsp/compiladores-trampolim-deque-e-thread-pool-dd1) onde compartilhei a minha experiência participando de uma competição de compiladores feita no twitter.

A grande mudança foi que, no momento do *parsing*, ao invés de converter diretamente para funções C do interpretador, o CRuby 1.9 passou a gerar um *bytecode intermediário*, que seria executado pela YARV. Neste processo então, poderiam ser feitas diversas otimizações antes de ser de fato gerado código de máquina, pelo que a YARV já vem precompilada com todas as funções C e chamadas de sistema, otimizando muito mais tempo e melhorando a performance do interpretador como um todo.

Além disso e outras melhorias no GC, o CRuby 1.9 trouxe ainda melhorias na questão de concorrência:

* as green threads foram substituídas por threads nativas (kernel threads), permitindo paralelismo em operações de I/O
* foi introduzido um modelo de "corrotina" com escalonamento 100% cooperativo, através da classe  [Fiber](https://ruby-doc.org/core-3.0.0/Fiber.html), permitindo escrever código concorrente leve sem necessidade de sincronização

<figure><img src="/files/RzbxI8PCOVedGlzKWgrs" alt=""><figcaption></figcaption></figure>

O **Ruby 1.9** foi um marco importante, melhorando drasticamente a performance e o suporte a concorrência!

> Ok Leandro, entendi. Mas o quê isso tem a ver com aquele lance lá que em Ruby parece que não há race condition?

## Global VM Lock (GVL)

Agora que entendemos o que é a *VM do Ruby* (YARV), podemos falar sobre o que aconteceu com o código que tentou simular uma condição de corrida (race condition), porém sem sucesso.

Durante o desenvolvimento do CRuby 1.9, ao mudar de green threads para kernel threads, era sabido que o uso de threads do sistema operacional (threads nativas) traria desafios relacionados à **race conditions**, como vimos no módulo anterior em C.

A nível de programação, isto não seria necessariamente um problema, pois, fornecendo estruturas de sincronização como **Mutex**, a pessoa programadora Ruby pode evitar race conditions protegendo seções críticas de código.

Entretanto, o interpretador CRuby 1.9 utiliza diversas estruturas de dados internas compartilhadas (como tabelas de métodos, objetos Ruby e coleções internas *do interpretador*) que precisam ser protegidas contra acesso simultâneo, mesmo em cenários onde o programa Ruby já utiliza Mutex para proteger recursos.&#x20;

A solução implementada para evitar **race conditions internas** foi a introdução de um *lock global na VM*, chamado de **GVL**, ou *Global VM Lock*.

### GVL e paralelismo

O GVL (também chamado de **GIL***,* ou *Global Interpreter Lock*) é um bloqueio global implementado no CRuby que garante que **apenas uma thread por vez execute código Ruby**, mesmo quando o programa possui várias kernel threads.

<figure><img src="/files/7RVhnNOoHEmwuxUDSfEK" alt="" width="563"><figcaption></figcaption></figure>

Como podemos ver, isso traz uma limitação com relação ao **paralelismo**. Mesmo com múltiplas threads, para o sistema operacional, é como se para todas as threads daquele processo existisse um "lock mutex", mesmo não sendo explícito.

Entretanto, o *GVL é liberado* em operações que envolvem **I/O** **bloqueante** - como leitura de arquivos ou sockets -, ou chamadas de funções C nativas, como *ffi*.

<figure><img src="/files/djVL0OAAowBS4JKtaWOV" alt="" width="563"><figcaption></figcaption></figure>

Com isso, podemos dizer que **múltiplas threads Ruby não paralelizam na CPU** mas por outro lad&#x6F;**,** *múltiplas threads Ruby podem rodar em simultâneo em operações de I/O.*

E é por isso que não conseguimos simular com sucesso a race condition, pois só podemos rodar **uma thread por vez na CPU.**

> Again, é como se já tivesse um "mutex embutido" no interpretador, que é liberado em operações de I/O

### GVL em ação: CPU-bound vs I/O -bound

Vamos ver em ação o GVL impedindo o paralelismo. No exemplo a seguir, temos uma tarefa **CPU-bound** (que faz uso intensivo de CPU) que, se executada, demora cerca de 0.07 segundos:

```ruby
require 'benchmark'

def fib(n) = n < 2 ? n : fib(n - 1) + fib(n - 2)
def cpu_task = fib(30)

time = Benchmark.measure { cpu_task }

puts "Tempo: #{time.real.round(2)} segundos"
```

No meu computador, um Macbook M1 Pro *late-2021* de 16GB, demorou 0.07 segundos:

```
Tempo: 0.07 segundos
```

Se precisarmos rodar 50 vezes, podemos querer executar em 50 threads diferentes:

```ruby
require 'benchmark'

def fib(n) = n < 2 ? n : fib(n - 1) + fib(n - 2)
def cpu_task = fib(30)

time = Benchmark.measure do
  threads = 50.times.map { Thread.new { cpu_task }}
  threads.each(&:join)
end

puts "Tempo: #{time.real.round(2)} segundos"
```

Se as threads rodassem em paralelo, poderíamos assumir que o tempo total ficaria um pouco acima dos 0.07 segundos, correto?

Mas o GVL não deixa rodar em paralelo quando temos uma tarefa CPU-bound:

```
Tempo: 3.69 segundos
```

Demorou quase 4 segundos!&#x20;

Agora outro exemplo. Simulando um `fib(30)` que demora 0.07 segundos, vamos fazer um `sleep(0.07)` que é uma operação **I/O-bound**, que faz uso intensivo de I/O. Com 50 threads, seria esperando um *paralelismo* totalizando em torno dos 0.07 segundos, correto?

```ruby
require 'benchmark'

def io_task
  sleep(0.07)
end

time = Benchmark.measure do
  threads = 50.times.map { Thread.new { io_task }}
  threads.each(&:join)
end

puts "Tempo: #{time.real.round(2)} segundos"
```

```
Tempo: 0.07 segundos
```

*OMG!* Estamos vendo o GVL em ação, **sendo liberado quando há uma operação de I/O**!

## A grande surpresa com o Ruby 3.4

Quero declarar aqui uma coisa. Durante os testes, me deparei com uma situação que eu não esperava. Não esperava **mesmo**.

&#x20;Vamos relembrar o código inicial deste tópico:

```ruby
balance = 0 # Variável compartilhada

# Cria 100 threads
threads = 100.times.map do
  Thread.new do
    500_000.times do
      balance += 1
    end
  end
end

# Espera as 100 threads terminarem de executar
threads.each(&:join)

puts "Balance is: #{balance} (expected: 50000000)"
```

Lendo o código, vemos um potencial para **race condition**, correto? Mas aprendemos ao longo deste tópico que o GVL garante que apenas **uma thread seja executada por vez na CPU,** o que faz com que este pequeno programa sempre retorne o saldo consistente, sem race condition.

> But...

Se não me falha a memória, em versões anteriores a 3.3, lembro que este mesmo programa, se **trocarmos o valor 1 por uma chamada de método que retorne o valor 1**, o resultado do programa seria diferente:

```ruby
balance = 0 # Variável compartilhada

def one = 1 # Um método que apenas retorna "1". Duh!

# Cria 100 threads
threads = 100.times.map do
  Thread.new do
    500_000.times do
      balance += one # Aqui trocamos "1" pela chamada do método.
                     # Matematicamente, são coisas equivalentes
    end
  end
end

# Espera as 100 threads terminarem de executar
threads.each(&:join)

puts "Balance is: #{balance} (expected: 50000000)"
```

Se executarmos este programa no **Ruby 3.2.2** (por exemplo), o resultado seria:

```
Balance is: 33693083 (expected: 50000000)
```

> Mas Leandro, cadê o GVL?

Então...é isto que eu esperava mesmo. Lembra que quando há chamada de I/O bloqueante o GVL é liberado? Em **instruções atômicas**, como *entrada e saída de funções/métodos*, o GVL também é liberado.&#x20;

E neste curto espaço de tempo, o escalonador do sistema operacional troca as threads e não temos controle de qual será executada. **Concorrência é linda, pois não?**

Porém, amigues, me veio a surpresa. Ao executar com **Ruby 3.4 ou Ruby 3.3**, eu esperava este mesmo comportamento, afinal, não vi nada na documentação informando que o Ruby 3.3 ou 3.4 trariam alguma mudança com relação *a como o GVL é liberado*.

Mas o que eu vi foi outra coisa:

<figure><img src="/files/bJ1WEVpxPTv8VXNek5Ai" alt=""><figcaption></figcaption></figure>

*OMFG!!!!!!!11111111one*

Eu procurei nas release notes do Ruby 3.3 e também do Ruby 3.4 e não encontrei nada que pudesse indicar o motivo disso ter mudado. Talvez analisando o [código do interpretador](https://github.com/ruby/ruby) em detalhes possa nos dar um pista.

> Quem souber me avisa, please

Acredito que houve muita melhoria feita na sincronização do GVL com relação a operações atômicas, por isto em Ruby 3.3+ este programa que eu trouxe não apresenta problemas com race condition.

Mas em versões anteriores, **operações atômicas liberam sim o GVL** e apresentam risco de race condition. Por isto, vamos explorar como sincronizar este programa com [**Mutex**](https://ruby-doc.org/core-3.0.0/Mutex.html).

## Sincronização com Mutex (utilizando a versão 3.2.2)

Vamos agora voltar à versão 3.2, já que a versão deste guia (3.4) não apresentou problemas de race condition.

A seguir, modificamos o programa, mas desta vez sincronizando o acesso com mutex:

```ruby
balance = 0 # Variável compartilhada

def one = 1
mutex = Mutex.new

# Cria 100 threads
threads = 100.times.map do
  Thread.new do
    500_000.times do
      mutex.synchronize do # Sincronização: o mutex é liberado no fim do bloco
        balance += one
      end
    end
  end
end

# Espera as 100 threads terminarem de executar
threads.each(&:join)

puts "Balance is: #{balance} (expected: 50000000)"
```

```
Balance is: 50000000 (expected: 50000000)
```

Yay!

## Sincronize sempre

Mesmo com versões mais recentes tendo uma melhor sincronização e eficiência do uso do GVL, não temos controle a nível de programa de quando o GVL é liberado. Portanto, em operações de potencial condição de corrida, sincronize sempre! Nunca sabemos como o programa irá se comportar.

Afinal,

> Isto, *senhoras e senhores*, é a maravilha da concorrência. Não temos controle algum sobre a ordem e execução das tarefas!

## Resumão

Este tópico em específico foi bem extenso, o que faz sentido pois são pontos cruciais sobre concorrência em Ruby. Vamos a um resumo do que foi abordado:

* **o quê é o interpretador do Ruby**: um pouco de história e como o Ruby 1.9 foi um marco no ecossistema com a introdução de uma VM e Kernel Threads
* **GVL e paralelismo**: motivo da existência do lock global da VM, e seus impactos no paralelismo de CPU
* **quando o GVL é liberado**: em algumas operações atômicas na CPU ou quando há chamada de I/O bloqueante, o GVL é liberado
* **a surpresa com o Ruby 3.4**: desenvolvimentos recentes no Ruby fazem ainda com que o GVL seja melhor utilizado, melhorando o desempenho e assertividade em programas concorrentes
* **sincronize sempre**: não confie no GVL. Se há risco de race condition, *mutex neles*!
* **threads e I/O**: sim, podemos usar multi-thread em operações de I/O, já vimos em ação que o GVL é liberado nestas situações. Teu web server escala HTTP requests tranquilamente, pode confiar

***

Calma Rubysta, ainda não terminamos. Vamos falar de modelo de atores, thread pool, I/O não-bloqueante, fibers. Nossa...tem muita coisa ainda pra ver em Ruby.&#x20;

*Stay tuned!*


# Modelo de Atores

Se você leu a primeira parte do guia, deve se lembrar das [propriedades de um processo](https://concorrencia101.leandronsp.com/parte-i-concorrencia-no-sistema-operacional/propriedades-de-um-processo):&#x20;

* processos têm **estado privado** e não compartilham memória
* processos se comunicam uns com os outros por **envio de mensagens** (IPC)
* processos têm um **identificador único** no sistema (PID)

Por conta destas propriedades, um processo não está sujeito aos memos problemas de **race conditions** igual às *threads -* embora seja possível se processos utilizarem mecanismos de compartilhamento de memória de forma explícit&#x61;*.*&#x20;

Com isto, podemos pensar numa possível abstração para trabalharmos com threads sem precisarmos recorrer ao uso de locks.

É como se tivéssemos "threads especiais" que iriam possuir as mesmas propriedades de um processo: estado privado, identificador único e comunicação por envio de mensagens.

Você acertou, estamos falando do **modelo de atores**.

> Muito bacana isso, Leandro. O kernel fornece tal estrutura?

*Não*. Temos de criar nossa própria abstração, a nível de **user space** (runtime).&#x20;

Existem diversas implementações de modelo de atores em diferentes linguagens de programação. Pra mencionar alguns casos, em **Java**, temos implementação de modelo de atores com a biblioteca *Akka***.** Em **Erlang**, a modelagem já faz parte das estruturas internas do runtime. E em **Ruby** (a partir da versão 3+), temos os *Ractors*.&#x20;

## Funcionamento do modelo de atores

Para entendermos o funcionamento do modelo de atores e como este se relaciona com Ruby, vamos pensar em como poderíamos implementar nosso próprio modelo. Não é difícil, acredite.

<figure><img src="/files/aB6LKvG478U0ePz9mqhr" alt=""><figcaption></figcaption></figure>

No exemplo acima, repare que, diferente da abstração de **Thread** que compartilha memória, um **Ator** não compartilha memória com outros atores. Isto por si só elimina problemas inerentes a condições de corrida.

> Acho que entendi, Leandro. Então quer dizer que todo ator usa uma kernel thread por trás?

*Depende*. Algumas implementações podem fazer com que cada ator seja mapeado diretamente para uma kernel thread, enquanto que em outras, um ator é uma abstração bastante leve dentro do runtime, que faz a *multiplexação* de atores para kernel threads conforme outros critérios, diminuindo assim o overhead com a criação de kernel threads.

Neste tópico, para fins didáticos, vamos **mapear cada ator diretamente para uma kernel thread**.

### 1. O ator deve ter estado privado

Para representar o ator, vamos mapear para uma *thread*:

```ruby
actor = Thread.new do
  # Lógica do ator aqui
end

actor.value # nil
```

O método `.value` faz a mesma coisa que o `.join` , mas traz o valor da **última expressão dentro do bloco**.

Por enquanto o ator não tem nenhuma lógica implementada. Vamos iniciar o estado **dentro da thread**:

```ruby
actor = Thread.new do
  state = 41
  # Faz coisas...
  state += 1
  state
end

actor.value # 42
```

A variável `state` foi criada apenas dentro do escopo da thread. Se tentarmos acessar a variável no escopo fora da thread, o programa lança uma exceção:

```ruby
state
# undefined local variable or method `state' for main:Object (NameError)
```

Para além disso, podemos passar argumentos para a thread no momento da criação, e receber os argumentos **dentro do escopo do bloco** da thread:

```ruby
actor = Thread.new(41) do |state|
  state += 1
  state
end

actor.value # 42
```

Foi passado para a thread o valor  absoluto `41` , mas podemos também passar variáveis:

```ruby
balance = 41

actor = Thread.new(balance) do |state|
  state += 1
  state
end

actor.value # 42 - a thread modificou seu estado interno
balance     # 41 - Wow! a thread não modificou o valor original de `balance`
```

Repare atentamente que a variável `balance` criada fora do escopo da thread não foi modificada, apesar da thread ter modificado seu estado interno. É isto que precisamos. Contudo, o quê aconteceu de fato ali?&#x20;

O Ruby, para alguns tipos de dados primitivos incluindo *números inteiros*, faz a cópia quando estes são enviados como argumentos para métodos, ou seja, **a passagem é feita por valor**.

Mas para outros tipos, como os *arrays e hashes*, não é feita a cópia mas sim a **passagem por referência**, pelo que a thread iria modificar o valor original, estando sujeita à race condition.&#x20;

Vamos a um exemplo de um ator que adiciona um elemento em um array:

```ruby
inbox = []

actor = Thread.new(inbox) do |queue|
  queue.push(42)
end

inbox # [42] - Ouch! O array original foi modificado pela thread! Not good...
```

Não é isto que queremos. O nosso "ator" está sendo capaz de modificar variáveis que foram criadas fora de seu escopo. E se forçarmos uma cópia explícita do array para a thread? Há solução pra isso em Ruby, com o método `.dup`  :&#x20;

```ruby
inbox = []

# No momento do dup, é literalmente feita uma cópia do array inteiro e passada 
# como argumento para a thread
actor = Thread.new(inbox.dup) do |queue|
  queue.push(42)
end

inbox # []
```

Prontinho, já conseguimos cumprir com o primeiro requisito para modelo de atores: **estado privado.** Vamos ver o próximo requisito.

### 2. O ator deve ter identificação única

Para que atores conversem uns com os outros, é preciso que cada um tenha uma identificação única, assim como os processos no SO têm PID.

Em Ruby, cada objeto tem um ID, e com **Thread** não é diferente:

```ruby
t1 = Thread.new {}
t2 = Thread.new {}
t3 = Thread.new {}

t1.object_id # 1594340
t2.object_id # 1594341
t3.object_id # 1594342
```

*Yay*! Já temos o segundo requisito cumprido. Vamos ao terceiro requisito e não menos importante: **envio de mensagens**.

### 3. O ator deve se comunicar por envio de mensagens

Levando em conta que a nossa implementação de ator é uma abstração em cima da **Thread**, como enviar mensagens para o ator? A classe `Thread` não fornece uma forma de enviar mensagens, então temos que criar o nosso próprio mecanismo.

```ruby
actor = Thread.new do
  # Implementação
end

# undefined method `send_message' for #<Thread (irb):1 run> (NoMethodError)
actor.send_message("Hello")
```

Pois é, simplesmente não funciona...não há nada na implementação de **Thread** que permita o envio de mensagens, pois a thread é apenas uma abstração em cima de **kernel threads** que são automaticamente escalonadas pelo sistema operacional.

Para implementar o envio de mensagens entre atores, vamos antes entender duas características principais sobre o envio: **síncrono e assíncrono**.

### 3.1. Envio de mensagens síncrono

Num modelo síncrono, um processo **Sender** precisa enviar uma mensagem a outro processo **Receiver**:

<div><figure><img src="/files/HZhbusqisKgP4lJEqwns" alt="" width="375"><figcaption></figcaption></figure> <figure><img src="/files/m2GdquO3dEHD0XuJuE7r" alt="" width="375"><figcaption></figcaption></figure></div>

Enquanto o receptor não confirma que recebeu e processou a mensagem, o processo que enviou a mensagem não consegue fazer outra tarefa, **portanto fica bloqueado**.

Mas o quê acontece caso o receptor este indisponível ou não tenha conseguido processar a mensagem? O processo **Sender** fica sem saber se a mensagem foi recebida, e a mensagem é descartada:

<figure><img src="/files/9OV9uE73Wk69r7W0poah" alt="" width="375"><figcaption></figcaption></figure>

Ou seja, além de ter deixado o **Sender** bloqueado, a *mensagem ficou perdida pra sempre*. Para implementar modelo de atores, precisamos fazer com que o ator seja capaz de **receber mensagens de forma assíncrona**.

### 3.2. Envio de mensagens assíncrono

No modelo assíncrono, a ideia é fazer com que a mensagem caia numa espécie de "caixa de correio" - similar ao que temos na vida real, ou então à *caixa de entrada* de email -, pelo que o ator fique verificando de *tempos em tempos* se chegou mensagem nova.

O ator é obrigado a **responder** tais mensagens? Não. Ou seja, neste modelo, o processo que envia a mensagem não necessariamente precisa ter uma resposta **síncrona** de que foi enviada, mas precisa ter a garantia de que a mensagem se encontra na caixa de entrrada.

<figure><img src="/files/URiHysbzNX6zLv80OhMc" alt="" width="563"><figcaption></figcaption></figure>

Neste modelo, o processo **Sender** não fica bloqueado, ou seja o **envio de mensagem foi assíncrono**. Como podemos implementar esta "caixa de entrada"?

### 3.3. Fila de mensagens

Vamos imaginar que neste caso as mensagens precisam ser processadas na ordem em que chegaram, correto? É um modelo onde *o primeiro que entra é o primeiro a sair*, que em inglês significa *first-in, first-out*, ou **FIFO**.

*FIFO* mesmo, você acertou, vamos implementar esta caixa de entrada com filas!&#x20;

> Que bem sabemos, filas podem ser implementadas com *arrays*!

<figure><img src="/files/ABkZTv6qdMFCtM4LIAB8" alt="" width="563"><figcaption></figcaption></figure>

<figure><img src="/files/MokqymjNz8GQWVCQg8Wr" alt="" width="563"><figcaption></figcaption></figure>

<figure><img src="/files/vZisvMjKguu8WeQWN5lU" alt="" width="563"><figcaption></figcaption></figure>

Em Ruby, poderíamos representar nossa caixa de entrada (fila) do ator como simplesmente **inbox,** e dentro do ator consumir mensagens desta fila com o método **pop:**

```ruby
inbox = []

actor = Thread.new(inbox) do |inbox|
  inbox.pop
end

actor.value # nil
```

E para *enviar mensagens* para a fila a partir do processo principal, temos o método **push:**

```ruby
inbox.push(42)
```

Entretanto, a esta altura o ator já terminou sua execução, portanto precisamos modificar o ator para que **fique em loop** verificando se há mensagens na inbox:

```ruby
inbox = []

actor = Thread.new(inbox) do |inbox|
  loop do 
    puts "Message: #{inbox.pop}"
  end
end

actor.join
```

O que temos na saída é isto:

```
Message:
Message:
Message:
Message:
Message:
Message:
Message:
Message:
Message:
....................
```

*Infinitamente*. Not good. Queremos que o ator fique meio que *suspenso* quando não houver mensagens na fila.&#x20;

Para resolver este problema, precisamos criar uma **fila bloqueante**, que bloqueia o ator para não ficar gastando CPU desnecessariamente num loop infinito.

### 3.4. Fila bloqueante (e thread-safe) de mensagens

A implementação de uma fila bloqueante pode ser feita com **exclusão mútua**, como já vimos em tópicos anteriores, onde:

* a thread verifica se há mensagens na fila:
  * se houver, faz o **pop** e volta ao início do loop
  * se não houver, utiliza **mutex** para enviar um sinal à thread que precisa ficar suspensa
* quando o processo **Sender** colocar mensagem na fila, é enviado um sinal à thread que está suspensa:
  * a thread retoma de onde parou, consome mensagem da fila e repete o voltando ao início do loop

<div><figure><img src="/files/dk4VrwISdlnyf7lkGZkE" alt="" width="563"><figcaption></figcaption></figure> <figure><img src="/files/cuqWIrNoR5neWsj4TrRq" alt="" width="563"><figcaption></figcaption></figure></div>

Precisamos recorrer a mutex e **condvar** para que a fila seja **thread-safe,** ou seja, a fila precisa ser compartilhada de forma segura entre threads, sem criar condições de corrida.

> Este conceito já foi explicado no módulo de [Concorrência em C](https://concorrencia101.leandronsp.com/parte-ii-concorrencia-em-diferentes-linguagens/concorrencia-em-c/thread-pool-em-c#mutex-e-condvar-em-c).

Além de Mutex, em Ruby também temos uma abstração para o uso de [Conditions](https://ruby-doc.org/core-3.0.2/ConditionVariable.html):

```ruby
inbox = []                                  # Representação da fila
mutex = Mutex.new                           # Mutex
condvar = ConditionVariable.new             # Condition

actor = Thread.new(inbox) do |inbox|
  # Thread fica em loop infinito
  loop do 
    mutex.synchronize do
      # Aqui, é enviado um "sinal" para a thread ficar em estado de WAIT
      #  no caso da fila estar vazia
      condvar.wait(mutex) if inbox.empty?

      # Quando a thread recebe o sinal de WAKE, continua o processamento
      puts "Received message: #{inbox.pop}"
    end
  end
end

mutex.synchronize do
  # Adiciona mensagem na fila e envia sinal de WAKE à thread que detém o mutex
  inbox.push(42)
  condvar.signal
end

# Espera 5 segundos antes de finalizar o programa, a título de vermos a mensagem
#  ser processada a tempo no terminal
sleep 5
```

```
Received message: 42
```

*Yay*! Que dia maravilhoso, não é mesmo? Inclusive, podemos implementar uma abstração de *Inbox* utilizando a técnica milenar de **um array, um mutex e uma variável condicional**:

```ruby
class ThreadSae
  def initialize
    @queue = []
    @mutex = Mutex.new
    @condvar = ConditionVariable.new
  end

  def send(message)
    @mutex.synchronize do
      @queue.push(message)
      @condvar.signal
    end
  end

  def receive
    @mutex.synchronize do
      @condvar.wait(@mutex) if @queue.empty?
      @queue.pop
    end
  end
end

inbox = BlockingQueue.new
inbox.send(42)

inbox.receive # 42
```

> Mas Leandro, sério que temos que implementar nossa própria fila bloqueante em Ruby?

Calma jovem, estamos com sorte hoje! Ruby já fornece uma classe pra isso, a `Thread::Queue` , que é totalmente *thread-safe*:

```ruby
queue = Thread::Queue.new

actor = Thread.new(queue) do |inbox|
  loop do 
    puts "Received message: #{inbox.pop}"
  end
end

queue.push(42)

sleep 3
```

Okay, agora que vimos os fundamentos de modelo de atores com exemplos em Ruby, já conseguimos cumprir com as principais características de um ator:

* estado privado
* identificação única
* envio de mensagens através de fila thread-safe

A seguir, vamos trazer uma série de exemplos impementando a abstração de **Ator** com todas as propriedades que já exploramos.

***

## Um ator que recebe mensagens

Vamos iniciar a implementação com uma classe Ruby bastante simples:

```ruby
class Actor
end
```

A seguir, no método initialize definimos a fila de mensagens e a **Thread** que irá ficar em background processando as mensagens:

```ruby
class Actor 
  def initialize
    @inbox = Thread::Queue.new

    Thread.new do 
      loop do
        # Implementação
      end
    end
  end
end
```

A implementação do ator é muito simples:

* lê mensagem da inbox
* processa mensagem:
  * se "exit", sai do loop e a thread é finalizada, encerrando o ator
  * em qualquer outro caso, imprime a mensagem recebida

```ruby
......
    Thread.new do 
      loop do
        message = @inbox.pop

        case message
        when :exit
          break
        else
          puts "Received message: #{message}"
        end
      end
    end
.....
```

Agora, no modelo de ator definimos um método `send` para o envio de mensagens para a fila e outro chamado `exit` que encerra o ator:

```ruby
class Actor 
  def initialize
    @inbox = Thread::Queue.new

    Thread.new do 
      loop do
        message = @inbox.pop

        case message
        when :exit
          break
        else
          puts "Received message: #{message}"
        end
      end
    end
  end

  def send(message)
    @inbox.push(message)
  end

  def exit
    @inbox.push(:exit)
  end
end

##############################

actor = Actor.new
actor.send(42)     # Envia mensagem para o ator (async)

sleep 3
```

Saída esperada:

```
Received message: 42
```

***

## Um ator que recebe mas também envia mensagens

Para além da capacidade de receber mensagens em uma **fila inbox**, um ator também deve ser capaz de enviar mensagens para o mundo externo, **sempre de forma assíncrona**, utilizando uma fila de mensagens.&#x20;

Mas ao invés de usar a inbox, podemos definir *outra fila*, que irá representar a **caixa de saída**, ou *outbox.*

<figure><img src="/files/PTcDjqN26ZwFzww0yofS" alt="" width="563"><figcaption></figcaption></figure>

> O envio de mensagens deve ser **sempre assíncrono,** ou seja, a mensagem deve ser colocada em alguma fila

Para ilustrar isso melhor,  vamos modificar o exemplo para representar uma conta bancária, que realiza depósitos e saques, mantendo um **estado privado** que é o saldo final da conta. Olha só que belezura fica isso em Ruby:

```ruby
class Account
  def initialize
    @inbox = Thread::Queue.new    # Caixa de entrada
    @outbox = Thread::Queue.new   # Caixa de saída
    @balance = 0                  # Estado inicial do ator

    Thread.new do 
      loop do
        message = @inbox.pop      # Consome mensagem

        # OMG! Pattern matching é mesmo lindo, não?
        case message
        in deposit: amount  then @balance += amount
        in withdraw: amount then @balance -= amount
        in :balance         then @outbox.push(@balance) # Coloca saldo na caixa de saída
        end
      end
    end
  end

  def deposit(amount)
    @inbox.push(deposit: amount)
  end

  def withdraw(amount)
    @inbox.push(withdraw: amount)
  end

  def balance
    @inbox.push(:balance)
    @outbox.pop # Lê a mensagem que o ator deixou na caixa de saída
  end
end

account = Account.new
account.deposit(100)
account.withdraw(50)

puts "Balance is: #{account.balance} (expected: 50)"
```

Este código dispensa maiores comentários, não acha?

***

## Um ator ultra genérico em Ruby

O nosso ator atual está amarrado com a lógica de *conta bancária*. E se criarmos uma abstração de **modelo de ator** que poderia funcionar com qualquer domínio de negócio?

Utilizando o conceito maravilhoso de **blocks**, podemos definir um ator super genérico que repassa os argumentos (estado inicial) e também o bloco de execução dinâmico:

```ruby
class Actor
  def initialize(*args, &block)
    # Define as filas de mensagens (entrada e saída)
    @inbox  = Queue.new
    @outbox = Queue.new

    # Definição da thread em background, que vai avaliar o bloco dinâmico passado para o ator
    Thread.new do
      # Execução do block e resultado de retorno, enviando os argumentos
      #  que foram passados para o ator. Estes argumentos definem o "estado"
      #  do ator e vão ser repassados para o bloco
      result = block.call(self, *args)

      # Utiliza o retorno do bloco para chamar um método que "transfere"
      #  o controle assíncrono, ou seja, coloca o resultado na caixa de saída
      self.yield(result)
    end

    # Após a instanciação do objeto do ator, retorna a própria instância para
    #  ser usada em outro contexto
    self
  end
end
```

A seguir, vamos definir 4 métodos no ator:

* `send` , que coloca mensagem na inbox
* `receive` , que lê mensagem da inbox
* `yield` , que colcoa mensagem na outbox
* `take` , que lê mensagem da outbox

Implementação final do ator genérico:

```ruby
class Actor
  def initialize(*args, &block)
    @inbox  = Queue.new
    @outbox = Queue.new

    Thread.new do
      result = block.call(self, *args)

      self.yield(result)
    end

    self
  end

  def receive
    @inbox.pop
  end

  def send(element)
    @inbox.push(element)
    self
  end

  def yield(element)
    @outbox.push(element)
  end

  def take
    @outbox.pop
  end
end
```

E agora, podemos utilizar este ator para diversos cenários, como no caso de uma conta bancária:

```ruby
account = Actor.new(0) do |instance, balance|
  loop do
    message = instance.receive

    case message
    in deposit: value  then balance += value.to_i
    in withdraw: value then balance -= value.to_i
    in :balance        then instance.yield(balance)
    end
  end
end

100.times do
  account.send(deposit: 1)
end

account.send(:balance)
balance = account.take

puts "Balance is: #{balance} (expected: 100)"
```

Ou ainda para outro contexto, como no caso de pontuação em um jogo:

```ruby
game = Actor.new(0) do |instance, score|
  loop do
    message = instance.receive

    case message
    in :score then instance.yield(score)
    in :increment then score += 1
    end
  end
end

42.times do
  game.send(:increment)
end

score = game.send(:score).take
puts "Score is: #{score} (expected: 42)"
```

> Que incrível, Leandro! Esse Ruby é mesmo lindo, não?

Sim, Ruby é demais. Mas calma que não paramos por aí. A partir da versão 3.0, Ruby trouxe uma abstração por cima da **Thread** que resolve o problema de race condition sem precisar de mutex, justamente implementando um **modelo de atores**, através da [classe Ractor](https://ruby-doc.org/core-3.0.2/Ractor.html), que faz *exatamente tudo o que implementamos até aqui.*

***

## Modelo de atores com Ractors

É isso mesmo, Ruby 3+ traz o conceito de atores prontinho pra gente, bastando definir como será o modelo conforme nosso requisito:

```ruby
account = Ractor.new(0) do |balance|
  loop do
    message = Ractor.receive # Lê mensagem da inbox

    case message
    in :balance        then Ractor.yield(balance) # Coloca mensagem na outbox
    in deposit: value  then balance += value.to_i
    in withdraw: value then balance -= value.to_i
    end
  end
end

account.send({ deposit: 100 }) # Envia mensagem para a inbox do ator
account.send({ withdraw: 50 }) # Envia mensagem para a inbox do ator

account.send(:balance) # Envia mensagem para a inbox do ator
balance = account.take # Lê mensagem da outbox do ator

puts "Balance is: #{balance} (expected: 50)"
```

Quero reforçar aqui que o Ractor em Ruby ainda está em fase experimental, portanto **não deve ser usado em produção**.

Entretanto, acredito que muito em breve teremos isto pronto pra produção. Atualmente temos uma excelente oportunidade para contribuir, deixando a implementação de Ractors em Ruby mais robusta ao longo do tempo.

### Ractors não acessam valores globais ou fora do contexto

Para que seja thread-safe, uma característica muito importante do Ractor é que não acessa valores globais:

```ruby
LIST = []

Ractor.new { LIST.push(42) }
```

O que lança um erro:

```
#<Thread:0x0000000102ec5740 run> terminated with exception (report_on_exception is true):
(irb):35:in `block in <top (required)>': can not access non-shareable objects in constant Object::LIST by non-main Ractor. (Ractor::IsolationError)
```

Uma forma de resolver é recebendo a lista no bloco, pelo que o Ruby irá **copiar** o valor de `LIST` para dentro do Ractor:

```ruby
Ractor.new(LIST) { |list| list.push(42) }

# LIST segue como array vazio, pois foi copiado inteiramente para dentro do Ractor
LIST
```

O mesmo vale para qualquer valor passado. É sempre **copiado**, e não movido:

```ruby
original_list = [1, 2, 3]

ractor = Ractor.new(original_list) do |copied_list|
  copied_list.push(42) # Modifica a cópia recebida
  copied_list
end

puts "Original LIST: #{original_list.inspect}" # Não foi alterada
puts "Modified list inside Ractor: #{ractor.take.inspect}" # A cópia foi alterada
```

### Movendo valores para dentro de um Ractor (alô, Rust!)

Já vimos que não é possível modificar um valor fora do contexto do Ractor pois é feita uma cópia. E se tentarmos enviar a lista como mensagem para o Ractor? Será que conseguimos?

```ruby
original_list = [1, 2, 3]

ractor = Ractor.new do
    list = Ractor.receive
    list.push(42)
end

ractor.send(original_list)

original_list # [1, 2, 3]   Original não modificado (ainda bem!)
ractor.take # [1, 2, 3, 42] Cópia modificada
```

O Ractor continua garantindo a integridade dos dados, completamente **thread-safe**! Entretanto, se quisermos **mover** a referência da lista para dentro do Ractor, podemos fazer passando o argumento `move: true` :&#x20;

```ruby
original_list = [1, 2, 3]

ractor = Ractor.new do
    list = Ractor.receive
    list.push(42)
end

ractor.send(original_list, move: true)
```

Vamos confirmar como está o valor da lista dentro do Ractor:

```ruby
ractor.take # [1, 2, 3, 42]
```

Continua na mesma, então vamos ver o valor original, se foi modificado ou não:

```ruby
original_list # #<Ractor::MovedObject:0x0000000107265810>
```

*Superb!* O valor foi **movido**, então isto significa que após o *move*, não podemos mais utilizar o valor original fora do contexto do Ractor, apenas dentro dele.

Desta forma, não foi feita uma cópia, mas sim um *move*.

> Salve Rustaceans, vocês estão se sentindo em casa agora, né?

### Filas podem ser construídas a partir de atores

Eu sei que você está agora só o meme da Nazaré, mas já vimos que para criar um ator, precisamos apenas de filas. Mas também podemos criar uma fila **thread-safe** (ou melhor, *ractor-safe*) baseada em Ractors.

Tudo o que precisamos é que o ator:

* fique bloqueado a espera de mensagens (`Ractor.receive`)
* repasse as mensagens para a caixa de saída (`Ractor.yield`)

```ruby
queue = Ractor.new do 
  loop do 
    Ractor.yield(Ractor.receive)
  end
end

queue.send(42)
queue.take # 42
```

*No way!* Acabamos de implementar uma fila utilizando Ractors! Agora me diga, caro leitor, quem vem primeiro? O **ovo ou a galinha**? Fica como lição de casa :smile:

<figure><img src="/files/LEpVmAjNjKyTlPcva1Jo" alt="" width="375"><figcaption><p>modelo de atores</p></figcaption></figure>

***

## Tolerância a falhas em Ruby com Ractors

O modelo de atores traz um desafio crucial  em sistemas baseados neste modelo. Processos assíncronos **vão falhar**. Não é uma questão de *se*, mas *quando*.

Quando entramos no mundo assíncrono com modelo de atores, temos que ter algum grau de **tolerência a falhas,** ou seja, lidar com erros e estabelecer planos de ação bem definidos para que os atores continuem operantes mesmo com falhas pontuais.

Em Ruby, infelizmente, neste momento não temos nada que implemente uma forma de **supervisionar Ractors que falham.**

> Mas a gente é maluco e implementa a nossa própria supervisão, né?

Com certeza. Antes de encerrar este tópico em Ruby, vamos implementar um modelo muito simples de supervisão.

### Um ator que representa uma conta bancária

Ainda no exemplo de uma conta onde é possível realizar depósitos e saques, a seguir definimos como é este modelo utilizando Ractors e o mais puro suco da **orientação a objetos**:

```ruby
class Account 
  def initialize(balance)
    @actor = Ractor.new(balance) do |balance|
      loop do
        message = Ractor.receive

        case message
        in deposit: amount  then balance += amount
        in withdraw: amount then balance -= amount
        in :balance         then Ractor.yield({ balance: balance })
        end
      end
    end
  end

  def deposit(amount)
    @actor.send({ deposit: amount })
  end

  def withdraw(amount)
    @actor.send({ withdraw: amount })
  end

  def balance
    @actor.send(:balance)
    @actor.take[:balance]
  end
end
```

* uma conta começa com um saldo inicial, enviado no construtor
* no construtor, temos a definição do **ator** que, através dos métodos públicos do objeto `Account` , recebe as mensagens para alterar o estado da conta bancária

<figure><img src="/files/HxCrCJorMpxMMNGbKLzj" alt="" width="563"><figcaption></figcaption></figure>

Criando um cenário "feliz", vamos manipular a conta bancária:

```ruby
account = Account.new(0)
account.deposit(100)
account.withdraw(50)

puts "Balance is: #{account.balance} (expected: 50)"
```

Tudo perfeito até aqui, mostrando o saldo corretamente na saída do programa. Agora, vamos adicionar um pouco de entropia, simulando um *crash* no ato&#x72;*:*

```ruby
# Adicionando mais matches na mensagem da inbox:

case message
in deposit: amount  then balance += amount
in withdraw: amount then balance -= amount
in :balance         then Ractor.yield({ balance: balance })
in :crash           then raise "Crash!" # <------------------------
end

.........
  def simulate_crash!
    @actor.send(:crash)
  end
.........
```

Simulamos o crash, então a seguir um depósito e a busca do saldo:

```ruby
account.simulate_crash!
account.deposit(200)

puts "Balance is: #{account.balance} (expected: 250)"
```

```
Balance is: 50 (expected: 50)

#<Thread:0x0000000103170468 run> terminated with exception (report_on_exception is true):
actor-supervisor.rb:11:in `block (2 levels) in initialize': Crash! (RuntimeError)
        from actor-supervisor.rb:4:in `loop'
        from actor-supervisor.rb:4:in `block in initialize'
<internal:ractor>:698:in `take': thrown by remote Ractor. (Ractor::RemoteError)
        from actor-supervisor.rb:28:in `balance'
        from actor-supervisor.rb:77:in `<main>'
actor-supervisor.rb:11:in `block (2 levels) in initialize': Crash! (RuntimeError)
        from actor-supervisor.rb:4:in `loop'
        from actor-supervisor.rb:4:in `block in initialize'

```

*Uh, oh!* No momento de buscar o saldo, o ator já não está mais saudável. Houve um crash que o deixou em estado de erro.&#x20;

Precisamos então *supervisionar* este ator, de modo a garantir que após o crash, o **funcionamento do ator continue normalmente**.

### Supervisionando um ator

Reforçando a implementação atual de `Account` :&#x20;

{% code lineNumbers="true" %}

```ruby
class Account 
  def initialize(balance)
    @actor = Ractor.new(balance) do |balance|
      loop do
        message = Ractor.receive

        case message
        in deposit: amount  then balance += amount
        in withdraw: amount then balance -= amount
        in :balance         then Ractor.yield({ balance: balance })
        in :crash           then raise "Crash!"
        end
      end
    end
  end

  def deposit(amount)
    @actor.send({ deposit: amount })
  end

  def withdraw(amount)
    @actor.send({ withdraw: amount })
  end

  def balance
    @actor.send(:balance)
    @actor.take[:balance]
  end

  def simulate_crash!
    @actor.send(:crash)
  end
end
```

{% endcode %}

Para supervisionar um ator, **precisamos de outro ator.** Se formos pensar no funcionamento deste supervisor, podemos concluir que:

* o supervisor é um ator, que:
  * cria um worker que será supervisionado
  * fica em loop "monitorando" o worker:
    * se está OK, salva o estado do worker e o coloca na outbox
    * se não está OK, cria outro worker com o estado atual do worker que falhou

<figure><img src="/files/M9pSMtJASTjVNnO6S0qq" alt="" width="563"><figcaption></figcaption></figure>

E o processo principal, tudo o que precisa é buscar na **outbox** do supervisor uma conta para poder manipular. Usando, *again*, OOP, conseguimos atingir isto de forma muito intuitiva:

```ruby
class AccountSupervisor
  def initialize
    @actor = Ractor.new do
      # Define o estado inicial (que é o atual também)
      #  e uma conta que será o "worker" supervisionado
      current_balance = 0
      account = Account.new(current_balance)

      loop do
        begin
          # Verifica se o worker está saudável
          response = account.ping
        
          if response && response[:msg] == 'PONG'
            # Se estiver saudável, atualiza o estado atual do worker no supervisor
            # e coloca o worker na caixa de saída (outbox)
            current_balance = response[:balance]
            Ractor.yield(account)
          else 
            # Caso contrário, lança um erro (não vamos tratar este erro por enquanto)
            raise "Account is not responding"
          end
        rescue Ractor::RemoteError, Ractor::ClosedError => e
          # Caso o worker não esteja saudável, como por exemplo sofreu um _crash_, 
          #  cria um novo worker utilizando o estado atual
          puts "[Supervisor] Account crashed with error: #{e.message}. Restarting..."
          account = Account.new(current_balance)
        end
      end
    end
  end

  # Método público que vai ser utilizado pelo processo princial para
  #  manipular uma conta (worker)
  def account
    @actor.take
  end
end
```

Precisamos implementar o método `ping` no worker e também adicionar a mensagem no matching de mensagens da inbox:

```ruby
class Account ......

case message
in deposit: amount  then balance += amount
in withdraw: amount then balance -= amount
in :balance         then Ractor.yield({ balance: balance })
in :crash           then raise "Crash!"
in :ping            then Ractor.yield({ msg: 'PONG', balance: balance })
```

```ruby
class Account .....

def ping
  @actor.send(:ping)
  @actor.take
end
```

<figure><img src="/files/1zA1vVyxXCQVUVrQELqk" alt="" width="563"><figcaption><p>representação final do ator <em>Account</em></p></figcaption></figure>

Agora, só falta ligar tudo, criando o worker a partir do **supervisor**:

```ruby
supervisor = AccountSupervisor.new
supervisor.account.deposit(100)
supervisor.account.withdraw(50)

puts "Balance is: #{supervisor.account.balance} (expected: 50)"
```

Até aqui tudo normal. Na saída conseguimos ver `Balance is: 50 (expected: 50)` .&#x20;

A seguir, simulamos um crash no worker:

```ruby
supervisor.account.simulate_crash!
```

*So far, so good*. Vamos fazer mais um depósito de *200*, pelo que queremos que o worker tenha sido reiniciado pelo supervisor e que ao final tenha o saldo atualizado para 250 (50 anteriormente + 200 de agora):

```ruby
supervisor.account.deposit(200)
puts "Balance is: #{supervisor.account.balance} (expected: 250)"
```

```
Balance is: 50 (expected: 50)

#<Thread:0x0000000104e4fd18 run> terminated with exception (report_on_exception is true):
actor-supervisor.rb:11:in `block (2 levels) in initialize': Crash! (RuntimeError)
        from actor-supervisor.rb:4:in `loop'
        from actor-supervisor.rb:4:in `block in initialize'
        
[Supervisor] Account crashed with error: thrown by remote Ractor.. Restarting...
Balance is: 250 (expected: 250)
```

Após o crash, podemos ver que o worker foi reiniciado e o novo depósito funcionou, sendo somado com o saldo anterior do ator que falhou.

*How cool is that?*

***

## Está gostando deste trabalho?

Se está gostando e considera colaborar para que mais trabalhos assim sejam feitos, não deixe de entrar para a [lista de **investidores premium**](https://concorrencia101.leandronsp.com/agradecimentos).&#x20;

> "Tá de sacanagem Leandro kk"

É sério, qualquer apoio financeiro via PIX é mais que bem-vindo, inclusive compartilhar o guia é uma excelente forma de apoiar o projeto também!

<figure><img src="/files/FUUkoZLpU19gvljXPbr1" alt="" width="283"><figcaption></figcaption></figure>

Ou copia e cola:

```
00020126850014BR.GOV.BCB.PIX013638ee4bde-574b-4197-b10f-68742087b00b0223Gratidão pelo cafezinho5204000053039865802BR5925Leandro Freitas Maringolo6009SAO PAULO62140510qrN6Ov1wRl63041A3C
```

No próximo tópico, vamos entrar no mundo do I/O não-bloqueante em Ruby. Apertem os cintos, pois a jornada ainda está longe do fim.


# Trabalhando com I/O

Antes de abordarmos I/O em Ruby, sugiro *fortemente* a leitura do módulo [**Trabalhando com I/O em C**](https://concorrencia101.leandronsp.com/parte-ii-concorrencia-em-diferentes-linguagens/concorrencia-em-c/trabalhando-com-i-o), onde eu trago conceitos importantes sobre I/O, suas limitações e funcionalidades de I/O não-bloqueante.

Para ilustrar como algumas operações de I/O são por natureza bloqueantes, vamos demonstrar a comunicação entre dois processos utilizando **UNIX named pipes**, ou *FIFO* (veja mais sobre arquivos *FIFO* [no meu artigo](https://dev.to/leandronsp/implementando-um-simples-background-job-com-unix-named-pipes-3eja)).

## Comunicação com FIFO

A primeira coisa que temos que fazer é criar um arquivo especial do tipo "pipe" com o comando `mkfifo` :

```bash
$ mkfifo queue

$ ls -l queue
prw-r--r-- 1 leandronsp leandronsp 0 Dec 31 23:59 queue
```

A saída começando com "p" indica justamente que este arquivo é especial, ou seja, um pipe FIFO.

## Reader

Vamos representar em Ruby o **leitor** da fila:

```ruby
# Abre o arquivo em modo somente leitura
File.open("queue", "r") do |file|
  # Lê o conteúdo do arquivo
  buffer = file.read(1024)

  # Exibe a mensagem recebida
  puts "Mensagem recebida: #{buffer}"
end
```

Ao executar o programa, note que ele fica **bloqueado** a espera que alguém escreva na fila.

## Writer <a href="#writer" id="writer"></a>

Em *outra janela do terminal*, podemos escrever no FIFO utilizando o comando `echo` :

Copy

```
$ echo Hello! > queue
```

Veja na janela do *writer* que a mensagem apareceu na saída padrão! *Superb!*

Outra característica importante pra notar aqui: o writer também fica bloqueado de escrever, caso não haja nenhum reader disponível.

> Isto é o puro suco do I/O bloqueante! Entretanto, I/O bloqueante apresenta algumas limitações, como já abordado no [módulo em C](https://concorrencia101.leandronsp.com/parte-ii-concorrencia-em-diferentes-linguagens/concorrencia-em-c/trabalhando-com-i-o#limitacoes-do-i-o-bloqueante)

## I/O não-bloqueante em Ruby

Vimos no tópico anterior que a chamada `File.open` por definição deixa o programa **bloqueado** até que os dados fiquem "prontos" no I/O durante a leitura. Para trabalharmos com I/O não-bloqueante em Ruby, não é muito diferente do que já vimos em C, pois basta chamar a syscall `open` com as devidas flags de `RDONLY` e `NONBLOCK` .

<pre class="language-ruby"><code class="lang-ruby"><strong># Abre o arquivo no modo não-bloqueante
</strong><strong>File.open("queue", File::RDONLY |File::NONBLOCK) do |file|
</strong>    # Lê o arquivo não-bloqueante e retorna os bytes lidos ou nil, caso o I/O não esteja pronto
    buffer = file.read(1024) 
    puts "Mensagem recebida: #{buffer}"
end
</code></pre>

Este simples programa irá terminar com a mensagem:

```
Mensagem recebida:
```

Por ser não-bloqueante, o programa **não fica bloqueado** e continua sua execução rumo ao fim do programa.

> Meio óbvio, não?

Agora vamos a um exemplo com loop, simulando um """""""loop de eventos""""""":

```ruby
File.open("queue", File::RDONLY |File::NONBLOCK) do |file|
  loop do
    buffer = file.read(1024)

    if buffer
      puts "Mensagem recebida: #{buffer}"
    else
      puts "Nenhum dado disponível no FIFO agora."
      sleep(1)
    end
  end
end
```

```
Nenhum dado disponível no FIFO agora.
Nenhum dado disponível no FIFO agora.
Mensagem recebida: Barata
Nenhum dado disponível no FIFO agora.
Nenhum dado disponível no FIFO agora.
Nenhum dado disponível no FIFO agora.
Nenhum dado disponível no FIFO agora.
```

*Meo deos do céoooo*, que dia maravilhoso!!!!!11

Contudo, esta solução ainda é bastante rudimentar e ineficiente para monitorarmos descritores de arquivos. Assim como em C, conseguimos em Ruby ter acesso à syscall `select` do sistema operacional através do módulo `IO` .

## Monitorando I/O com select

Monitorar descritores de arquivos com **select** em Ruby é *flocos com morango*:

```ruby
fifo = File.open('queue', File::RDONLY | File::NONBLOCK) 

loop do
  # Usa IO.select para monitorar o descritor de arquivo
  ready = IO.select([fifo], nil, nil, 1) # Timeout de 1 segundo

  if ready.nil?
    puts "Nenhum descritor disponível. Continuando..."
    next
  end

  # Verifica se há dados no FIFO
  ready[0].each do |io|
    data = io.read(1024)
    puts "Mensagem recebida: #{data}"
  end
end
```

```
Nenhum descritor disponível. Continuando...
Nenhum descritor disponível. Continuando...
Mensagem recebida: Barata
enhum descritor disponível. Continuando...
Nenhum descritor disponível. Continuando...
```

*OMG!* Que dia incrível!

***

Até agora, exploramos os recursos que Ruby oferece para realizar operações de I/O de forma *não-bloqueante*. Essa característica muda fundamentalmente a maneira como escrevemos código, afastando-nos da abordagem tradicional síncrona para adotar uma abordagem assíncrona.

Normalmente, escrevemos código de forma síncrona, onde as funções e rotinas são executadas em sequência e os dados necessários já estão disponíveis na memória. Com I/O não-bloqueante, perdemos essa certeza. Não sabemos quando os dados estarão prontos, o que nos obriga a estruturar o código para lidar com a disponibilidade futura das informações.

Essa mudança nos leva ao **assincronismo**, onde não esperamos pela conclusão de uma operação, mas configuramos o código para reagir quando a operação for concluída.

## I/O assíncrono em Ruby

Uma vez que vimos formas de lidar com I/O não-bloqueante em Ruby e monitorar descritores, chegou o momento de explorar I/O assíncrono em Ruby.&#x20;

Antes de avançar, vamos retomar o exemplo da leitura do FIFO de forma não-bloqueante com **select**:

{% code lineNumbers="true" %}

```ruby
fifo = File.open('queue', File::RDONLY | File::NONBLOCK) 

loop do
  # Usa IO.select para monitorar o descritor de arquivo
  ready = IO.select([fifo], nil, nil, 1) # Timeout de 1 segundo

  if ready.nil?
    puts "Nenhum descritor disponível. Continuando..."
    next
  end

  # Verifica se há dados no FIFO
  ready[0].each do |io|
    data = io.read(1024)
    puts "Mensagem recebida: #{data}"
  end
end
```

{% endcode %}

O bloco entre a linha 13 e 16 indica que temos uma lógica arbitrária para lidar com a mensagem que chega no FIFO, no caso uma lógica simples que *imprime no STDOUT* a mensagem recebida.

Repare que esta lógica pode ser aplicada para qualquer I/O que fique pronto, portanto podemos *levar* esta lógica para outra estrutura de **lazy evaluation**, funcionando como um **callback**. Em Ruby, podemos recorrer ao uso de *lambdas*.

```ruby
fifo = File.open('queue', File::RDONLY | File::NONBLOCK) 

# Callback para quando dados estiverem disponíveis
on_data_available = ->(data) do 
  puts "Mensagem recebida: #{data}"
end

loop do
  # Usa IO.select para monitorar o descritor de arquivo
  ready = IO.select([fifo], nil, nil, 1) # Timeout de 1 segundo

  if ready.nil?
    puts "Nenhum descritor disponível. Continuando..."
    next
  end

  # Verifica se há dados no FIFO
  ready[0].each do |io|
    data = io.read(1024)
    on_data_available.call(data) # Uso do callback
  end
end
```

### O problema dos callbacks

Embora callbacks sejam uma solução funcional para lidar com I/O assíncrono, eles podem rapidamente se tornar difíceis de gerenciar à medida que a complexidade do código cresce. Esse problema é conhecido como **callback hell**, onde o código tem um certo nível de *indireção*, tornando a lógica fica fragmentada e difícil de entender.

```ruby
fifo = File.open('queue', File::RDONLY | File::NONBLOCK)

# CALLBACK HELL!!!!!1
on_data_available = ->(data) do 
  process_message(data) do |processed_data|
    log_message(processed_data) do |log_status|
      notify_clients(log_status)
    end
  end
end

loop do
  ready = IO.select([fifo], nil, nil, 1)
  next if ready.nil?

  ready[0].each do |io|
    data = io.read(1024)
    on_data_available.call(data)
  end
end
```

O problema começa a se tornar evidente:

* Cada callback aninhado adiciona uma nova camada de indireção
* O código fica menos intuitivo e mais difícil de depurar
* &#x20;O fluxo de controle não é óbvio, tornando a manutenção mais complexa

Para evitar o **callback hell**, temos de pensar numa solução que permita um certo tipo de **controle** sobre o código, trazendo mais *direção*, ou seja, permitir lidar com código assíncrono de maneira mais *linear* e retomar a execução do código de maneira *mais explícita,* de **forma cooperativa***.*

**Fibers** *to the rescue*.

## Escalonamento cooperativo com Fibers

Se você leu a primeira parte do guia e também o módulo em C, já está familiarizado com o conceito de *escalonamento cooperativo*.

[Fibers](https://docs.ruby-lang.org/en/master/Fiber.html) são estrutuas leves de concorrência em Ruby que permitem que sejam pausadas e retomadas **explicitamente no código**. Diferente das Threads, as Fibers nunca são escalonadas preemptivamente, e portanto seguem um modelo **cooperativo** de concorrência.

> Apesar de serem utilizadas para diversos propósitos, como por exemplo processamento de fluxo de dados e simulação de corrotinas, Fibers são muito úteis no contexo de I/O não-bloqueante pois trazem esta característica de *cooperação*, resolvendo o problema de indireção no código assíncrono.

No código de exemplo com callbacks, vamos refatorar para o uso de Fibers:

```ruby
fifo = File.open('queue', File::RDONLY | File::NONBLOCK) 

monitor = Fiber.new do
  loop do
    # Usa IO.select para monitorar o descritor de arquivo
    ready = IO.select([fifo], nil, nil, 1) # Timeout de 1 segundo

    if ready.nil?
      # Retorna o controle para o loop principal até que haja dados disponíveis
      puts "Nenhum descritor disponível. Continuando..."
      Fiber.yield 
    else
      ready[0].each do |io|
        data = io.read(1024)
        # Retorna o controle para o loop principal com os dados lidos
        Fiber.yield(data) if data
      end
    end
  end
end

# Loop principal
loop do
  data = monitor.resume
  next unless data

  puts "Mensagem recebida: #{data}"
end
```

Primeiramente criamos uma fiber chamada **monitor**, que fica em loop infinito monitorando o I/O com *select*. Em seguida, temos o *loop principal*, que controla a execução da fiber de monitoramento.

* Dentro da fiber **monitor**, quando não há dados disponíveis, o controle é devolvido ao loop principal com **Fiber.yield**
* Ainda na fiber **monitor**, quando de fato há dados disponíveis, o controle também é devolvido ao loop mas é enviado junto os dados do I/O
* No loop principal, é feita a exeução da Fiber (*resume*), e caso haja dados disponíveis, estes são enviados para o STDOUT com *puts*

```
Nenhum descritor disponível. Continuando...
Nenhum descritor disponível. Continuando...
Nenhum descritor disponível. Continuando...
Nenhum descritor disponível. Continuando...
Nenhum descritor disponível. Continuando...
Nenhum descritor disponível. Continuando...
Mensagem recebida: Barata
Nenhum descritor disponível. Continuando...
Nenhum descritor disponível. Continuando...
Mensagem recebida: Barata
Nenhum descritor disponível. Continuando...
```

Que dia **M.A.R.A.V.I.L.H.O.S.O**!

Com Fibers, o código fica mais **linear**, tornando o fluxo de execução mais próximo de um código **síncrono**, facilitando a leitura e manutenção. E temos também **menos indireção**, evitando o *callback hell*.

No entanto Fibers não eliminam completamente a complexidade do código assíncrono, mas ajudam a tornar a estrutura mais legível e menos fragmentada. Com o auxílio de um padrão de **loop de eventos**, podemos deixar o código ainda mais legível e fácil de entender.

## Event Loop

Quando combinadas com event loops, Fibers fornecem um excelente modelo para I/O assíncrono em Ruby, sem precisar recorrer a threads pesadas.

Não é difícil criarmos nosso próprio **event loop** que fica registrando de escalonando Fibers de forma cooperativa.

Basicamente, o loop precisa de:&#x20;

* Uma estrutura de *fila* que guarda as fibers registradas
* Um método para registrar fibers na fila
* Outro método que consome fibers da fila e as executa enquanto estiverem em execução

> Lembrando do modelo cooperativo: a fiber devolve o controle com **yield**, e o código fora da fiber (no caso o event loop) executa a fiber com **resume**

```ruby
class EventLoop
  def initialize
    @queue = []
  end

  def schedule(&block)
    @queue << Fiber.new(&block)
  end

  def run
    while @queue.any?
      fiber = @queue.shift # Pega a próxima Fiber da fila

      if fiber.alive?
        fiber.resume # Executa a fiber ou continua a execução
        @queue << fiber # Reinsere no final da fila se ainda estiver ativa
      end
    end
  end
end
```

Agora, para adaptar nosso exemplo de leitura assíncrona do FIFO, podemos de forma super simples utilizar o loop de eventos:

{% code lineNumbers="true" %}

```ruby
event_loop = EventLoop.new

event_loop.schedule do
  fifo = File.open('queue', File::RDONLY | File::NONBLOCK)
  loop do
    ready = IO.select([fifo], nil, nil, 1)

    if ready.nil?
      puts "Nenhum descritor disponível. Continuando..."
      next
    else 
      ready[0].each do |io|
        data = io.read(1024)
        puts "Mensagem recebida: #{data}"
      end
    end
  end
end

event_loop.schedule do
  loop do
    puts "Monitorando status do sistema..."
  end
end

event_loop.run
```

{% endcode %}

Na linha 3, o método **schedule** cria e registra a fiber de monitoramento no loop de eventos. E na linha 20, o **schedule** cria e registra outra fiber que pode representar outra operação qualquer, **tudo de forma assíncrona!**

E pra finalizar, o método **run** da linha 26 é simplesmente o gatilho para iniciar o loop e escalonamento das fibers.

> *Lindo, não?* Quem diria, e você aí pensando que loop de eventos era uma invenção super inovadora do Javascript no backend, né?

## Escalonador de Fibers no Ruby 3+

Com o lançamento do Ruby 3, o core da linguagem introduziu um novo recurso: **Fiber Scheduler**, uma [interface nativa para escalonamento de Fibers](https://docs.ruby-lang.org/en/3.2/Fiber/Scheduler.html). Esse recurso permite que I/O assíncrono ocorra de forma transparente, sem necessidade de manipular diretamente event loops ou utilizar o **select**.

Esse avanço coloca Ruby mais próximo de outras linguagens que já possuem suporte nativo para concorrência assíncrona, como JavaScript (async/await) e Go (goroutines).

A implementação de um escalonador de Fibers permite que operações de I/O sejam automaticamente não bloqueantes quando usamos Fibers. Isto significa que, quando uma Fiber precisar esperar por I/O, o Ruby pode suspender automaticamente e permitir que outras tarefas rodem na mesmo thread.

Antes do Ruby 3, para conseguir esse comportamento, precisávamos criar nosso próprio event loop - como fizemos anteriormente -, mas agora, basta fornecer um escalonador customizado, e o runtime cuida do restante.

***

No próximo e último tópico de Ruby, iremos abordar algumas gems que lidam com concorrência em Ruby, incluindo *Celulloid, parallel e Async*.&#x20;

**Fiquem ligades!**


