> 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/trabalhando-com-i-o.md).

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