> For the complete documentation index, see [llms.txt](https://concorrencia101.leandronsp.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://concorrencia101.leandronsp.com/parte-ii-concorrencia-em-diferentes-linguagens/concorrencia-em-ruby/race-condition-yarv-gvl-e-paralelismo-em-ruby.md).

# 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!*
