BT

Disseminando conhecimento e inovação em desenvolvimento de software corporativo.

Contribuir

Tópicos

Escolha a região

Início Notícias Melhores da InfoQ em 07: O Futuro de Threads em Ruby

Melhores da InfoQ em 07: O Futuro de Threads em Ruby

Esta notícia foi originalmente publicada em 23 de maio e faz parte da coleção das melhores notícias de 2007 publicadas na InfoQ

Uma entrevista recente com Matz (Yukihiro Matsumoto), criador do Ruby, e Sasada Koichi, criador do YARV, cuida do tópico de lidar com threads no Ruby. Os lançamentos estáveis atuais do Ruby usam threads em espaço de usuário (também chamadas "green threads"), que significa que o interpretador Ruby toma conta de tudo que tem a ver com threads. Isso em contraste com threads de kernel, onde a criação, agendamentos e sincronização é feito com chamadas de sistema (syscalls) do Sistema Operacional, o que torna essas operações custosas, pelo menos comparado com seus equivalentes em threads de espaço de usuário. Threads de espaço de usuário, por outro lado, não conseguem usar múltiplos núcleos ou múltiplas CPUs (porque o sistema operacional não sabe sobre elas e portanto não consegue cronogramá-los nesses núcleos/CPUs).

Ruby 1.9 recentemente integrou YARV como a nova Ruby VM, que, dentre outras mudanças, trouxe threads de kernel ao Ruby. A introdução de threads de kernel (ou "threads nativas") foi amplamente aplaudida, particularmente por desenvolvedores vindos de Java ou .NET onde threads de kernel são o comum. Entretanto, existe um porém. Sasada Koichi explica:

 Como vocês sabem, YARV suporta threads nativas. Isso significa que você pode rodar cada thread Ruby como uma thread nativa concorrente

Isso não significa que toda Ruby thread roda em paralelo. YARV tem uma trava global de VM (global interpreter lock) que somente uma thread Ruby sendo executada tem. Essa decisão talvez nos faça feliz porque podemos rodar a maioria das extensões escritas em C sem qualquer modificação.

Isso significa: não importa quantos núcleos ou CPUs estão disponíveis, apenas uma thread Ruby será capaz de rodar de cada vez. Existem atalhos e extensões nativas podem lidar com o Global Interpreter Lock (GIL) de maneiras mais flexíveis, por exemplo, liberá-las antes de iniciar uma operação que demora muito tempo. Sasada Koichi explica a API disponível para liberar a GIL:

Você deve liberar a trava gigante de VM antes de fazer alguma tarefa bloqueadora. Se precisar fazer isso em bibliotecas de extensão, use a API rb_thread_blocking_region().

rb_thread_blocking_region(
  blocking_func, /* função que vai bloquear */
  data,          /* isso será passada à função acima */
  unblock_func   /* se outra thread causar uma exceção com Thread#raise,
                    essa função é chamada para desbloquear ou NULL
)


O problema: isso efetivamente remove o grande argumento para threads de kernel, o uso de múltiplos núcleos ou CPUs, enquanto retiver seus problemas.

Threads de kernel também é a razão porque Continuações devem ser removidas de futuras versões de Ruby. Continuações são maneiras de agendamento cooperativo, o que significa que uma thread de execução explicitamente dá o controle para outra. Essa funcionalidade também é conhecida sobre o nome de "Co-rotina", e está por aí por um bom tempo. Recentemente, ele reapareceu aos olhos do público por causa do framework web baseado em Smalltalkbased Seaside, que usa Continuações para significativamente simplificar aplicações web.

A  aproximação usando threads de Kernel com um GIL é comparável ao sistema de thread do Python, que também usa uma GIL, e fez isso por um bom tempo. O GIL do Python causou inúmeros debates sobre como removê-la, mas ele permaneceu por ali por todo esse tempo.

Entretanto, uma olhada nos Guido van Rossum, pensamentos de Guido van Rossum, criador do Python, sobre threads, dá uma visão de um futuro alternativo para threads em Ruby. Em um post recente sobre GIL, Guido van Rossum explica:

Apesar de tudo, vocês estão certos sobre a GIL não ser tão ruim quanto inicialmente se pensa: você apenas precisa desfazer a lavagem cerebral que ganhou do Windows e Java que parecem considerar threads como a única maneira de se aproximar de atividades concorrentes.

Apenas porque Java inicialmente era para ser um sistema operacional de pequenos dispositivos que não suportava múltiplos espaços de endereçamento, e apenas porque criação de processos no Windows costumava ser lento como um cão, isso não significa que múltiplos processos (com uso correto de IPC) não sejam uma aproximação melhor para escrever aplicações para múltiplas CPUs do que para threads.

Diga Não para os maus combinados de travas, deadlocks, granularidade de travas, livelocks, não-determinismo e condições de corrida.

Os benefícios de threads agendadas preemptivamente que compartilham um espaço de endereço já foi muito debatida. Unix foi, por muito tempo, de thread única ou com threads em espaço de usuário. Paralelismo foi implementado com múltiplos processos que se comunicavam via diferentes meios InterProcess Communication (IPC), como Pipes, FIFOs, ou explicitamente compartilhando regiões de memória. Isso foi suportado pela fork chamada de sistema fork, que permitia duplicar processos em execução com baixo custo.

Recentemente, linguagens como Erlang ganharam muito interesse também pela aproximação de não compartilhar nada (chamada de "processos leves) + métodos fáceis de IPC. Os "processos leves" não são processos de sistema operacional, mas de fato vivem dentro do mesmo espaço de endereços. Eles são chamados "processos", porque não podem olhar nas áreas de memórias dos outros. O "leve" vem pelo fato deles serem manipulados por um agendador em espaço de usuário. Por muito tempo, isso significou que Erlang tinha os mesmos problemas de outros sistemas com threads em espaço de usuário: sem suporte para múltiplos núcleos ou múltiplas CPUs e chamadas de sistema que bloqueavam todas as threads. Recentemente, entretanto, isso foi resolvido adotando uma aproximação m:n: o runtime Erlang agora usa múltiplas threads de kernel, e cada uma roda um agendador de espaço de usuário. Isso significa que Erlang agora tira vantagem de múltiplos núcleos e CPUs, sem mudar o modelo de execução.

Por sorte do lado Ruby, a equipe Ruby está consciente disso e está considerando esse futuro para Ruby:

 [...] se tivermos múltiplas instâncias de VMs em um processo, essas VMs podem rodar em paralelo. Vou trabalhar nesse tema para um futuro próximo (como meu tópico de pesquisa).

 [...] se existir múltiplos problemas em threads nativas, vou implementar green threads. Como sabem, isso tem vantagens contra threads nativas (criação de threads leves, etc). Será um hack adorável (para sua informação, minha tese de graduação é para implementar uma biblioteca de threads de espaço de usuário em nossa CPU SMT específica).

Isso indica que versões de threads de espaço de usuário (green) de Ruby não está fora de questão, particularmente em luz dos  problemas de implementação de sistemas de threads em diferentes sistemas operacionais, como esse:

Programar em threads nativas tem suas próprias dificuldades. Por exemplo, no Mac OS X, exec() não funciona (causa uma exceção) se outras threads estiverem rodando (um problema de portabilidade). Se encontrarmos problemas críticos em threads nativas, eu farei uma versão de green threads no trunk (YARV).

Por que existe a necessidade para a solução de Múltiplas VMs (MVM) do Sasada Koichi? Rodar múltiplos interpretadores Ruby e fazê-los comunicar via métodos de IPC (por exemplo, sockets) é possível hoje também. Entretanto, isso vem com seus próprios problemas:

  • O processo Ruby precisa executar um novo interpretador Ruby, o que significa que ele precisa saber que foi lançado (qual executável Ruby usar). Isso rapidamente se torna difícil de fazer de maneira portável. Por exemplo: se o JRuby é usado, o executável precisa ser "jruby". Pior: a JVM ou servidor de aplicação rodando não pode permitir rodar programas externos.
  • O novo interpretador Ruby precisa ser configurado com as variáveis de ambiente corretas, LOADPHATs, Include Paths, e o arquivo .rb principal para executar.
  • Comunicação pode acontecer via DRb, mas isso precisa ir via rede que é a única maneira portável de IPC.
  • Comunicação de rede significa negociar portas (em qual porta a parte "servidor" dos dois programas deve escutar).
  • Comunicação de rede também significa problemas potenciais com firewalls que reclamam de programas abrindo conexões ou portas.

Claro, esses problemas fazem isso ser muito mais complicado do que o equivalente Thread de lançar uma nova thread de execução:

x = Thread.new{ 
   p "hello"
}

Ou este exemplo em Erlang:

pid_x =  spawn(Node, Mod, Func, Args) 

Esse código Erlang lança um novo processo leve, process, e de fato: esse é todo o código que se precisa. Todo o código de configuração para os problemas explicados acima são tratados por baixo dos panos.
TO pid é a maçaneta de um novo processo, e permite, por exemplo, comunicação simples:

pid_x ! a_message

Isso envia uma simples mensagem ao processo com o pid armazenado me pid_x. A mensagem pode consistir de vários tipos, por exemplo Atoms, a versão Erlang dos símbolos de Ruby.

IPC simples como essa é certamente possível em Ruby também. Erlectricity, é uma nova biblioteca que permite comunicação entre Erlang e Ruby, mas ele poderia também ser usada para funcionar entre VMs Ruby. IPC de Erlang é particularmente interessante, já que usa uma aproximação baseada em padrões de procura que facilita a passagem de mensagens e torna tudo mais conciso.

O Ruby MVM é certamente a idéia mais promissora para o futuro de threads em Ruby. Isso evita os problemas da GIL e da manipulação manual de processos Ruby e usa as idéias de não compartilhar nada que tornam Erlang e outros sistemas mais atraentes para concorrência.

JRuby é a única versão de Ruby que utiliza threads de kernel, mais porque está rodando sobre a JVM que suporta isso. O custo de criar threads de kernel é meio fora de medida por causa do uso de pools de thread (threads são criadas e mantida por ali até serem necessárias). Detalhes do suporte de threads do IronRuby não são conhecidas ainda, mas já que a CLR e a JVM são bem similares, é provável que threads de kernel serão usadas também.

Uma possibilidade de prototipar e experimentar com a idéia de uma Ruby MVM seria de lançar múltiplas instâncias de JRuby no mesmo processo da JVM e fazê-los comunicar entre si. Isso efetivamente teria o mesmo IPC barato (dados trafegando simplesmente passando ponteiros, mas isso se os dados forem apenas para leitura).

Ola Bini recentemente escreveu sobre sua nova idéia, de jrubysrv, que permite rodar múltiplas instâncias de JRuby em uma JVM para salvar memória.

Como parece, os detalhes do futuro de suporte de thread em Ruby ainda não estão decididos e podem ser bem diferentes das implementações alternativas.

Avalie esse artigo

Relevância
Estilo/Redação

Conteúdo educacional

BT