Threads Puma no Ruby e o GIL, qual a co-relação?

Threads Puma no Ruby e o GIL, qual a co-relação?
Photo by Adrien Delforge / Unsplash

No mundo do desenvolvimento Ruby, uma questão comum é como o servidor web Puma consegue utilizar threads eficientemente, mesmo com a presença do Global Interpreter Lock (GIL) no MRI (Matz's Ruby Interpreter). Este post explora como as bibliotecas são projetadas para trabalhar com multithreading em Ruby, apesar das limitações impostas pelo GIL.

O que é o GIL?
O GIL é um mecanismo usado no MRI para limitar a execução de bytecodes a uma única thread por vez. Isso significa que, mesmo em um ambiente com múltiplos núcleos de CPU, apenas uma thread pode executar código Ruby em um dado momento. O GIL foi introduzido para simplificar o gerenciamento de memória e evitar condições de corrida.

Puma e Threads em Ruby:
Puma é um servidor web rápido e concorrente para Ruby/Rack que é capaz de lidar com múltiplas requisições simultaneamente. Ele faz isso usando threads, mas como isso é possível com o GIL?

  1. I/O Bound vs CPU Bound:
    • I/O Bound: Operações que dependem principalmente de I/O (Entrada/Saída), como leitura de arquivos ou requisições de rede, podem ser bloqueadas ou aguardar dados. Durante essas operações, o GIL é liberado, permitindo que outras threads executem.
    • CPU Bound: Operações que dependem do processamento da CPU são limitadas pelo GIL, pois apenas uma thread pode executar código Ruby por vez.

No desenvolvimento de software, as operações podem ser classificadas como CPU bound ou I/O bound. Essa classificação ajuda a entender como otimizar aplicações e escolher a melhor estratégia de paralelismo ou concorrência.

Operações CPU Bound:
São aquelas operações onde o fator limitante é a velocidade de processamento da CPU. Em outras palavras, são tarefas que são intensivas em termos de cálculos e processamento de dados.

Exemplos de Operações CPU Bound:

  1. Processamento de Imagens:
    • Redimensionamento, filtragem ou transformações de imagens são intensivos em CPU.
  2. Cálculos Científicos:
    • Simulações físicas, operações matemáticas complexas, como algoritmos de Machine Learning.
  3. Criptografia e Descriptografia:
    • Processos de encriptar e desencriptar dados são intensivos em termos de cálculos.
  4. Compilação de Código:
    • Transformar código-fonte em código executável é uma tarefa que exige muito da CPU.
  5. Renderização de Gráficos 3D:
    • Processos envolvidos na renderização de gráficos tridimensionais, como em jogos ou simulações.

Operações I/O Bound:
São operações onde o fator limitante é a velocidade de leitura/escrita de dados em um sistema de armazenamento ou rede. Aqui, a CPU fica ociosa à espera da conclusão da operação de I/O.

Exemplos de Operações I/O Bound:

  1. Leitura/Escrita em Disco:

    • Operações como ler ou escrever arquivos grandes em um disco rígido.
  2. Requisições de Rede:

    • Fazer chamadas a APIs externas, onde a espera pela resposta da rede é o fator limitante.
  3. Operações de Banco de Dados:

    • Consultas que envolvem leitura/escrita intensiva em um banco de dados, especialmente se a rede ou o disco são lentos.
  4. Streaming de Dados:

    • Carregar ou enviar grandes quantidades de dados, como streaming de vídeo ou áudio.
  5. Backup e Transferência de Arquivos:

    • Copiar grandes volumes de dados de um local para outro.
  6. Concorrência com Threads:

    • Puma aproveita o fato de que muitas aplicações web são predominantemente I/O bound. Enquanto uma thread está aguardando uma resposta de I/O, outras threads podem continuar processando outras requisições.
    • Isso permite que Puma atenda várias requisições simultaneamente, aumentando a eficiência e a capacidade de resposta do servidor.
  7. Configuração de Threads em Puma:

    • Puma permite configurar o número de threads que ele usará. Isso é feito no arquivo de configuração puma.rb.
    • Exemplo de configuração:
      threads_count = ENV.fetch("MAX_THREADS") { 5 }
      threads threads_count, threads_count
      
    • Aqui, threads_count define o número mínimo e máximo de threads que o Puma pode utilizar.

Design de Bibliotecas para Multithreading:

  • Bibliotecas em Ruby que são projetadas para serem "thread-safe" levam em consideração o GIL e a natureza concorrente do ambiente.
  • Elas evitam compartilhar estado entre threads e utilizam mecanismos de sincronização, como mutexes, para gerenciar o acesso a recursos compartilhados.

Embora o GIL no MRI Ruby limite a execução paralela de código Ruby, ele não impede a concorrência, especialmente em aplicações I/O bound. Puma explora essa característica para fornecer um servidor web altamente concorrente e eficiente. Entender como o GIL funciona e como configurar adequadamente o Puma é crucial para otimizar aplicações Ruby para o máximo desempenho.

Puma e I/O Bound Operations:
A chave para entender como o Puma funciona eficientemente, apesar do GIL, está na natureza das aplicações web. A maioria das operações em uma aplicação web é I/O bound, não CPU bound. Isso significa que as threads passam a maior parte do tempo esperando por operações de I/O (como leitura/escrita de banco de dados, chamadas de API, etc.) para serem concluídas.

Multithreading com I/O Bound Operations:
Quando uma thread está bloqueada aguardando uma operação de I/O, o GIL é liberado, permitindo que outra thread assuma e execute. Isso significa que, mesmo com o GIL, o Puma pode ter várias threads atendendo diferentes requisições simultaneamente, desde que essas requisições estejam esperando por I/O.

Exemplo Prático:
Imagine uma aplicação Rails que recebe várias requisições de rede. Quando uma thread inicia uma chamada de rede, ela fica bloqueada aguardando a resposta. Durante esse tempo, o GIL é liberado, e outra thread pode começar a processar uma nova requisição. Isso permite que o Puma lide com várias requisições de forma eficiente, mesmo em um ambiente com GIL.

Limitações:
Embora essa abordagem funcione bem para operações I/O bound, ela não melhora o desempenho para operações CPU bound. Para tarefas que exigem intensa computação, o GIL ainda limita a execução paralela, e outras abordagens, como processos múltiplos ou JRuby (uma implementação Ruby sem GIL), podem ser mais adequadas.

O Puma aproveita a natureza I/O bound das aplicações web para oferecer um desempenho eficiente em um ambiente Ruby com GIL. Ao alternar entre threads durante operações de I/O, o Puma consegue lidar com várias requisições simultaneamente, tornando-o uma escolha popular para servidores web em aplicações Ruby/Rails.

Read more