All for Joomla All for Webmasters
Computação Paralela Ruby

Threads em Ruby: paralelizando requisições HTTP

Programas tradicionais geralmente executam em uma única linha sequencial de execução (single thread). Dividir o processo a ser executado em múltiplas threads com tarefas que podem ser executadas de forma concorrente traz um ganho significativo ao desempenho do algoritmo, principalmente com a popularização de processadores multi-núcleo, onde as threads são executadas realmente de forma simultânea. Neste post vou mostrar como paralelizar web crawler em ruby que implementei anteriormente.

Em Ruby, em sua biblioteca padrão, as threads que o programa em execução lança só conseguem tirar proveito do desempenho de um núcleo do processador, devido ao Global Interpreter Lock (GIL), que assegura que somente um código Ruby seja executado ao mesmo tempo. Ainda assim, é possível tirar proveito do seu desempenho em situações onde seu algoritmo faz múltiplas chamadas de entrada/saída (como requisições HTTP à uma API externa, por exemplo), nas quais o programa em execução fica bloqueado aguardando a resposta para então seguir com a próxima requisição. Com múltiplas threads, ao aguardar uma resposta, a thread em execução pode dar lugar a uma outra que vai disparar uma nova requisição, otimizando o tempo de execução real.

Fluxo de execução com Global Interpreter Lock

Ao utilizar esse paradigma de programação, entretanto, é necessário ter em mente a forma concorrente em que seu algoritmo vai ser processado. Isso implica que você não tem garantia da ordem de execução de suas threads, o que pode causar condições de corrida, onde o algoritmo gera uma saída inesperada dependendo da ordem de execução. No caso do web crawler implementado, cada requisição feita para recuperar um post individual pode ser executada de forma independente sem prejuízo de resultado. Essa tarefa de buscar uma postagem individual é o trecho de código que vai ser disparado para diferentes threads executarem.

O CÓDIGO-FONTE

Em relação ao código original, poucas modificações. No cabeçalho, são requisitadas as bibliotecas thread thwait (que permite delimitar um ponto onde é esperado todas as threads finalizarem a sua execução). Todas as threads criadas com o código do bloco Thread.new são adicionadas a um array que será o argumento da função de espera. No caso em questão, adicionei as postagens mineradas a um array para demonstrar que em alguns casos é interessante aguardar a finalização da execução de todas as threads, a fim de garantir que o array estará completamente populado quando for utilizado posteriormente. Em um caso de aplicação onde cada postagem seria persistida individualmente em um banco de dados, por exemplo, essa espera não seria necessária.

É possível, ainda nesse código, paralelizar também as requisições às páginas de categoria, que possuem a mesma característica. À medida que essa lista cresce, cresce também o ganho em termos de tempo de execução. Em testes empíricos, a execução de algoritmos com essas características chega a ser cerca de 10 vezes mais rápida quando as requisições são feitas de forma concorrente. Existem outras gems e possibilidades menos triviais de fazer esse tipo de implementação, que podem ser mais eficientes dependendo do contexto da sua aplicação. Quaisquer dúvidas ou sugestões, utilize o campo de comentários ou entre em contato!

Você Também Pode Gostar

1 Comentário

  • Responder
    Sannytet
    12 de dezembro de 2018 at 01:46

    Nice posts! 🙂
    ___
    Sanny

  • Deixe uma resposta para Sannytet Cancelar Resposta