# 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="https://1347906346-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FADWkcUWoXJQdqxwxxE7z%2Fuploads%2FUEjggnI1yYxMFtbKIu9S%2FScreenshot%202025-01-09%20at%2002.23.01.png?alt=media&#x26;token=37c95287-76c7-4e2c-8b76-582d83d436bf" 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="https://1347906346-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FADWkcUWoXJQdqxwxxE7z%2Fuploads%2FE4ASIkiSyjggqtzzD4V5%2FScreenshot%202025-01-15%20at%2000.29.15.png?alt=media&#x26;token=70fd4291-8ddc-4a19-8686-680faf789180" alt="" width="375"><figcaption></figcaption></figure> <figure><img src="https://1347906346-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FADWkcUWoXJQdqxwxxE7z%2Fuploads%2FmKNsCCm933zqibvdPY58%2FScreenshot%202025-01-15%20at%2000.33.13.png?alt=media&#x26;token=f432d72f-7e0f-4f30-b908-0abcfd7b6d63" 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="https://1347906346-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FADWkcUWoXJQdqxwxxE7z%2Fuploads%2FhCwTOSrMczDOxgwhdNSf%2FScreenshot%202025-01-15%20at%2000.34.06.png?alt=media&#x26;token=ace26604-3603-470f-9d53-8b45682c7835" 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="https://1347906346-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FADWkcUWoXJQdqxwxxE7z%2Fuploads%2F4V1G6NRkyUo5AK9VsleU%2FScreenshot%202025-01-15%20at%2000.45.58.png?alt=media&#x26;token=2d90712f-9820-40bf-ac24-fc479468bf1d" 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="https://1347906346-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FADWkcUWoXJQdqxwxxE7z%2Fuploads%2FXvV2fPIm1nf1S84H5Nos%2FScreenshot%202025-01-15%20at%2000.52.12.png?alt=media&#x26;token=77ffccd7-8e75-4d5b-b3f7-faac82243ff8" alt="" width="563"><figcaption></figcaption></figure>

<figure><img src="https://1347906346-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FADWkcUWoXJQdqxwxxE7z%2Fuploads%2FQ6mG9k3Xryg7iwYvOprr%2FScreenshot%202025-01-15%20at%2000.53.50.png?alt=media&#x26;token=fd25fd73-2337-4d20-8f86-8c72be4854a7" alt="" width="563"><figcaption></figcaption></figure>

<figure><img src="https://1347906346-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FADWkcUWoXJQdqxwxxE7z%2Fuploads%2FpTirpIhwhtVMvA3QShKR%2FScreenshot%202025-01-15%20at%2000.54.17.png?alt=media&#x26;token=67ccb190-4a7f-4370-bb12-916b2b559d6b" 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="https://1347906346-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FADWkcUWoXJQdqxwxxE7z%2Fuploads%2F8s4JIW6UgK4WgZqPkl0L%2FScreenshot%202024-12-27%20at%2021.28.16.png?alt=media&#x26;token=fadb065d-275a-4ecb-9795-47b5eaf2e2b2" alt="" width="563"><figcaption></figcaption></figure> <figure><img src="https://1347906346-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FADWkcUWoXJQdqxwxxE7z%2Fuploads%2FD9KcBk3VA8in6uwPqsjH%2FScreenshot%202024-12-27%20at%2021.28.37.png?alt=media&#x26;token=145f941e-07e0-492c-9634-84da94514371" 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="https://1347906346-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FADWkcUWoXJQdqxwxxE7z%2Fuploads%2FBKuuaPVDnGAVUcaT7Itc%2FScreenshot%202025-01-15%20at%2002.00.57.png?alt=media&#x26;token=8c9270c6-f591-4ebb-a2bb-92919e35a393" 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="https://1347906346-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FADWkcUWoXJQdqxwxxE7z%2Fuploads%2FLkgofSYxa1cOZ6sw1ett%2Ffernanda-torres.jpg?alt=media&#x26;token=3388f842-8bc6-4b88-86d3-5c02732a2b00" 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="https://1347906346-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FADWkcUWoXJQdqxwxxE7z%2Fuploads%2FKVVqXagBFP5XlZv7sojm%2FScreenshot%202025-01-15%20at%2021.52.38.png?alt=media&#x26;token=8d99e38e-ffa3-4fee-baf7-1204c9152b63" 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="https://1347906346-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FADWkcUWoXJQdqxwxxE7z%2Fuploads%2F2Uy6jDm8m6UkiDcwSGX1%2FScreenshot%202025-01-15%20at%2021.26.02.png?alt=media&#x26;token=81ed8e90-01fa-4c28-8c95-76b8d992e0c3" 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="https://1347906346-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FADWkcUWoXJQdqxwxxE7z%2Fuploads%2FAhP8DiqYNXHY8v17dZ7N%2FScreenshot%202025-01-15%20at%2021.55.12.png?alt=media&#x26;token=d5790876-b7ef-4961-869d-b3081c34546c" 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="https://1347906346-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FADWkcUWoXJQdqxwxxE7z%2Fuploads%2F7xEda0Tzw1MwnyvSpRVg%2FScreenshot%202024-12-11%20at%2023.49.01.png?alt=media&#x26;token=00528cb5-27e7-45ba-b13f-5d6a7e20da3d" 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.
