Programação paralela utilizando Node.js

Érico Souza Loewe
CWI Software
Published in
8 min readAug 26, 2019

Tradicionalmente, softwares tem sido escrito de maneira serial, mas isso vem cada vez mais se tornando um problema, pois, a cada dia, o uso de tecnologia aumenta, junto com a quantidade de aplicações rodando, isso resulta em um uso gigantesco de energia e tempo. Com o paralelismo, conseguimos diminuir ambos os fatores, resultando em um ganho tanto para a aplicação quanto para o meio ambiente.

Programação paralela é a programação de múltiplos computadores, ou de computadores com múltiplos processadores internos, para resolver um problema com uma velocidade melhor do que o mesmo seria resolvido com um único computador. Isso também oferece a oportunidade de trabalharmos com problemas maiores, pois, devido a estarmos utilizando mais de um computador/processador, geralmente temos mais poder de memória e processamento.

Esse tipo de desenvolvimento é considerado muito mais complicado do que programas sequenciais, uma vez que, com o paralelismo se tem múltiplas threads para se controlar e os dados podem ser logicamente e fisicamente distribuídos. Vendo que:

  • Comunicação e sincronização (e.g deadlocks) são difíceis de serem rastreados;
  • O não determinismo em um programa paralelo faz seu comportamento difícil de se entender;
  • Particionamento de dados, mapeamento de processos e sincronização podem atrapalhar na performance.

Apesar das dificuldades encontradas no desenvolvimento de aplicações em paralelo, é possível se ter um grande ganho no tempo de processamento dependendo da arquitetura ou modelo utilizado. Além do tempo, a programação paralela faz mais proveito dos recursos energéticos [Mór et al. 2010], pois com um algoritmo paralelo bem programado é possível utilizar os recursos à disposição de maneira eficiente, fazendo com que um número mínimo de unidades de processamento que estão ligadas fiquem ociosas, aumentando, assim, o speedup. Na atualidade, ainda existem limites físicos para o aumento da velocidade de um único processador, além do alto custo associado ao desenvolvimento desse tipo de processadores.

Modelos de programação

Quando se trata sobre a transferência dos dados entre as tarefas em paralelo, temos os seguintes modelos:

Variáveis Compartilhadas

Pode ser implementado de diversas maneiras em uma linguagem que tenha suporte ao paralelismo. Delas: Sub-rotinas, Laços, Blocos básicos Comandos ou operações, Instruções, etc. [Sato et al. 1996]

No java, a comunicação entre threads funciona dessa maneira [Barcellos 2002]. Ela é feita através do compartilhamento de um espaço comum de endereçamento, disponibilizado através de um DSM (esquema de memória compartilhada distribuída) que mantém a semântica do modelo de memória.

Passagem de Mensagem

Geralmente utilizado para implementar algoritmos que irão utilizar de mais de um computador para realizar o processamento [Sato et al. 1996] ou por programas que necessitam trocar dados entre processos. Esse modelo pode ser visto em linguagens como Node.js que utiliza do IPC para transferência de dados entre processos. [njs 2019b]

Speedup e eficiência

Um dos objetivos de se utilizar o paralelismo é o aumento na velocidade de processamento pela subdivisão do algoritmo em tarefas que podem ser executadas concorrentemente. Para garantir que se está tendo um ganho de velocidade, é utilizado o cálculo Speedup (Sn) e Eficiência (En), sendo:

  • Speedup a razão entre o tempo gasto na execução sequencial T1, e o tempo gasto da mesma execução em paralelo Tn.
Calculo do Speedup
  • Eficiência a razão entre o Speedup (Sn) e o número de vezes (n) que a execução foi dividida.
Calculo da Eficiência

Node.js e cases de programação paralela

Node.js é uma plataforma de código aberto (open source) construída sobre o motor JavaScript do Google Chrome para facilmente construir aplicações de rede rápidas e escaláveis. Node.js usa um modelo de I/O direcionada a evento não bloqueante que o torna leve e eficiente, ideal para aplicações em tempo real com troca intensa de dados através de dispositivos distribuídos.

Diversos serviços da internet tem migrado para Node.js, como por exemplo: PayPal e LinkedIn, trazendo, aos mesmos, um ganho na performance de suas aplicações e facilidade no desenvolvimento. No caso da PayPal, a migração foi feita de Java para Node.js e isso trouxe uma melhoria no tempo de resposta ao cliente de 35%, dobrou a quantidade de requests por segundo e reduziu 33% o número de linhas de código.

Programação paralela utilizando Node.js

Programação paralela
Programação paralela

Apesar de Node.js ser projetado sem threads [Foundation 2019], não significa que não é possível se tirar proveito de múltiplos cores em uma aplicação. Podemos dividir o processamento com a criação de processos filhos a partir da API do child_process.fork() o qual foi feito para lidar com essas situações.[Foundation 2019] Também é possível utilizar o modulo cluster, o qual permite o compartilhamento de sockets entre processos para permitir um melhor balanceamento da sua aplicação entre os cores. [Foundation 2019]

Serão mostrados 2 exemplos nesse artigo, um utilizando o módulo cluster para realizar o paralelismo de requisições feitas a um servidor http (5.2), e o outro exemplo irá utilizar a API do child_process.fork() para dividir o processamento de uma longa requisição http (5.1).

Exemplo utilizando o método fork do modulo ChildProcess

Exemplo real do uso do modulo child_process, onde o Server recebe uma requisição complexa e não bloqueia as demais requisições devido ao uso do fork no calculo complexo

O método child_process.fork() foi desenvolvido para iniciar uma nova instância do Node.js em outro processo com um canal comunicação IPC estabelecido que permite a troca de mensagens entre o processo pai e filho.

O exemplo que será apresentado basicamente é composto por 2 módulos (fork, calculo), um responsável por iniciar o servidor e receber uma requisição do tipo ’/calculo’ e realizar um fork do modulo calculo.js. O outro modulo é responsável por realizar o calculo e enviar o resultado do mesmo ao processo pai.

console.log(`Master ${process.pid} iniciou`);

http.createServer((request, response) => {
if (request.url === '/calculo') {
const worker = child_process.fork('calculo.js');

worker.send(myNumber);
worker.on('message', soma => {
response.end(`Soma: ${soma}`);
});
} else {
response.end('Outra requisicao operando normalmente...')
}
}).listen(3000);

Modulo responsável pela criação de um servidor http, gerenciamento das requisições e criação do processo filho o qual irá realizar a operação complexa Para as requisições que do tipo ’/calculo’ será executado o processamento em paralelo a partir da abertura de um worker, envio do valor ao mesmo e adição de um listener do tipo message para receber o valor a ser processado pela suposta soma.

// calculo.js
console.log(`Worker ${process.pid} iniciou`);

process.on('message', (myNumber) => {
const soma = umMetodoQualquerComUmCalculoComplexo(myNumber);

process.send(soma);
console.log(`Worker ${process.pid} acabou`);
});

Modulo responsável por realizar o cálculo e enviar o resultado ao processo pai a cada mensagem que recebe

Na segunda parte do exemplo é possível visualizar que o modulo calculo estará na escuta do evento que será acionado pelo processo pai, onde irá receber myNumber, executar o calculo complexo e então retornar o resultado a partir do método process.send.

Exemplo utilizando o modulo cluster

Exemplo real do uso do modulo cluster, onde cada Server poderia ser um fork do modulo principal direcionado em portas diferentes, distribuindo assim as requisições do servidor

Utilizando o módulo cluster é possível dividir o processamento de aplicações em sub processos, onde, se tem o processo principal, o qual é responsável por gerenciar os subprocessos, e subprocessos (workers) os quais nesse exemplo, irão iniciar uma instancia de um servidor http.

Basicamente o exemplo apresentado é composto por 2 métodos (rodarPrincipal() e rodarWorker()), um responsável em iniciar os sub processos (Listing 3) e o outro por iniciar um servidor http em seu processo (Listing 4), ambos se encontram no mesmo arquivo, e o gerenciamento de qual irá iniciar é feito a partir de uma validação de uma propriedade disponível no modulo cluster (cluster.isMaster).

function rodarPrincipal() {
console.log(`Master ${process.pid} is running`);

for (let i = 0; i < numeroDeCPUs; i++) {
const worker = cluster.fork();
}
}

Método responsável por iterar sobre o número de CPU’s (número de processadores e núcleos) disponíveis, e sobre cada um realizar o fork da aplicação utilizando o método fork do modulo cluster (cluster.fork())

No exemplo mostrado, é possível visualizar que o modulo cluster permite a criação de processos filhos com facilidade. Cada worker é criado a partir da API do child_process.fork().

function rodarWorker() {
console.log(`Worker ${process.pid} iniciou`);

http.createServer((request, response) =>
response.end(`processo ${process.pid} falou oi!`)
).listen(3000);
}

Método responsável pela criação de um servidor http a cada chamada realizada do método

O modulo cluster permite facilmente fazer um cluster do servidor, sem a necessidade de configurações complexas, pois, para cada processo filho criado, o Node.js compartilham entre eles todas as portas do servidor.

Conclusões

Programação paralela tem grande importância no desenvolvimento de aplicações, pois ela auxilia em seu tempo de processamento, diminuindo, assim, o consumo de energia necessária para rodá-las. Foi possível verificar com esse artigo que existem diversas definições do conceito de programação paralela, isso devido à abrangência do assunto, mas se optou a utilizar o modelo em que utilizamos de um único computador com uma ou mais CPU’s.

Facilidades ao se utilizar Node.js

Além de toda facilidade que se tem em se desenvolver na linguagem Javascript (linguagem utilizada pela plataforma), o Node.js — como é possível verificar nos exemplos apresentados no projeto — foi desenvolvido para facilitar a transferência de dados entre processos e abertura de novas instâncias da aplicação a partir do (fork), o que traz um grande ganho para o desenvolvimento de aplicações que necessitam de paralelismo.

Dificuldade encontradas com Node.js

Devido ao Node.js ser projetado sem threads [Foundation 2019] não é possível criar um bloco de código o qual irá ser processado paralelamente.

Referências

  • Child Process. Node.js Foundation. Disponível em https://nodejs.org/docs/latest/api/child_process.html [Online]; acessado 18 Maio 2019.
  • Cluster. Node.js Foundation. Disponível em https://nodejs.org/docs/latest/api/cluster.html [Online]; acessado 18 Maio 2019.
  • Barcellos, M. (2002). Programação paralela e distribuída em java. [Online]; acessado 18 Maio 2019.
  • Barry, W. (2006). Parallel Programming: Techniques and Applications Using Networked Workstations and Parallel Computers, 2/E. Pearson Education India, 2nd edition.
  • Cantelon, M., Harter, M., Holowaychuk, T., and Rajlich, N. (2014). Node.js in Action. Manning Greenwich.
  • da Silva, L. N. (2006). Modelo híbrido de programação paralela para uma aplicação de elasticidade linear baseada no método dos elementos finitos. [Online]; acessado 18 Maio 2019.
  • Engineering, P. (2013). Node.js at paypal. https://medium.com/paypal-engineering/node-js-at-paypal-4e2d1d08ce4f. [Online]; acessado 18 Maio 2019.
  • Foundation, N. (2019). About node.js®. https://nodejs.org/en/about/. [Online]; acessado 18 Maio 2019.
  • Mór, S. D., Alves, M., Lima, J. V., Maillard, N., and Navaux, P. O. (2010). Eficiência energética em computação de alto desempenho: Uma abordagem em arquitetura e programação para green computing. XXXVII Seminário Integrado de Software e Hardware-SEMISH, pages 346–360.
  • Pian, W. C. T. L. and Turner, S. J. (1995). A framework for visual parallel programming.
  • Prasad, Kiran, K. N. and Coatta., T. (2014). Node at linkedin: The pursuit of thinner, lighter, faster. Commun. ACM, 57(2):44–51.
  • Sato, L. M., Midorikawa, E. T., and Senger, H. (1996). Introdução a programação paralela e distribuída. Anais do XV Jornada de Atualização em Informática, Recife, PE, pages 1–56.

--

--