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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://concorrencia101.leandronsp.com/parte-ii-concorrencia-em-diferentes-linguagens/concorrencia-em-ruby/trabalhando-com-i-o.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
