A prática de trabalhar com threads no Node.js. 10.5.0

Mais recentemente, a versão 10.5.0 da plataforma Node.js. foi lançada. Um de seus principais recursos foi o suporte ao trabalho com fluxos que foram adicionados ao Node.js. enquanto ainda eram experimentais. Esse fato é especialmente interessante, pois a plataforma agora tem essa oportunidade, cujos aderentes sempre se orgulharam do fato de não precisar de fluxos devido ao fantástico subsistema de E / S assíncrona. No entanto, o suporte ao thread apareceu no Node.js. Por que isso seria? Para quem e por que eles podem ser úteis?



Em poucas palavras, isso é necessário para que a plataforma Node.js atinja novos patamares nas áreas em que anteriormente não apresentava os resultados mais notáveis. Estamos falando de realizar cálculos que usam intensamente os recursos do processador. Esse é principalmente o motivo pelo qual o Node.js não é muito forte em áreas como inteligência artificial, aprendizado de máquina e processamento de grandes quantidades de dados. Muito esforço foi direcionado para permitir que o Node.js se mostrasse bem na solução desses problemas, mas aqui essa plataforma ainda parece muito mais modesta do que, por exemplo, no desenvolvimento de microsserviços.

O autor do material, cuja tradução publicamos hoje, diz que decidiu reduzir a documentação técnica, que pode ser encontrada na solicitação de recebimento original e em fontes oficiais , a um conjunto de exemplos práticos simples. Ele espera que qualquer pessoa que veja esses exemplos saiba o suficiente para iniciar os threads no Node.js.

Sobre o módulo worker_threads e o sinalizador --experimental-worker


O suporte a multithreading no Node.js é implementado como um módulo worker_threads . Portanto, para tirar proveito do novo recurso, este módulo deve ser conectado usando o comando require .

Observe que você só pode trabalhar com worker_threads usando o worker_threads - experimental-worker ao executar o script, caso contrário, o sistema não encontrará este módulo.

Observe que a bandeira inclui a palavra "trabalhador", não "segmento". Exatamente o que estamos falando é mencionado na documentação, que usa os termos "thread de trabalho" (thread de trabalho) ou apenas "trabalhador" (trabalhador). No futuro, seguiremos a mesma abordagem.

Se você já escreveu código multiencadeado, explorando os novos recursos do Node.js, verá muitas coisas com as quais você já está familiarizado. Se você nunca trabalhou com algo assim antes, continue lendo mais, pois explicações apropriadas para os novatos serão fornecidas aqui.

Sobre tarefas que podem ser resolvidas com a ajuda de trabalhadores no Node.js


Os fluxos de trabalho destinam-se, como já mencionado, a resolver tarefas que usam intensivamente os recursos do processador. Deve-se notar que seu uso na solução de problemas de E / S é um desperdício de recursos, pois, de acordo com a documentação oficial, os mecanismos internos do Node.j, destinados a organizar a E / S assíncrona, são muito mais eficientes do que usar resolvendo o mesmo problema do fluxo de trabalho. Portanto, decidimos imediatamente que não trataremos da entrada e saída de dados usando trabalhadores.

Vamos começar com um exemplo simples que demonstra como criar e usar trabalhadores.

Exemplo No. 1


 const { Worker, isMainThread,  workerData } = require('worker_threads'); let currentVal = 0; let intervals = [100,1000, 500] function counter(id, i){   console.log("[", id, "]", i)   return i; } if(isMainThread) {   console.log("this is the main thread")   for(let i = 0; i < 2; i++) {       let w = new Worker(__filename, {workerData: i});   }   setInterval((a) => currentVal = counter(a,currentVal + 1), intervals[2], "MainThread"); } else {   console.log("this isn't")   setInterval((a) => currentVal = counter(a,currentVal + 1), intervals[workerData], workerData); } 

A saída desse código será semelhante a um conjunto de linhas que mostram contadores cujos valores aumentam em velocidades diferentes.


Os resultados do primeiro exemplo

Vamos lidar com o que está acontecendo aqui:

  1. As instruções dentro da expressão if criam 2 threads, cujo código, graças ao parâmetro __filename , é obtido do mesmo script que o Node.js passou quando o exemplo foi executado. Agora, os trabalhadores precisam do caminho completo para o arquivo com o código, eles não suportam caminhos relativos, e é por isso que esse valor é usado aqui.
  2. Os dados para esses dois trabalhadores são enviados como um parâmetro global, na forma do atributo workerData , que é usado no segundo argumento. Depois disso, o acesso a esse valor pode ser obtido através de uma constante com o mesmo nome (observe como a constante correspondente é criada na primeira linha do arquivo e como, na última linha, é usada).

Aqui está um exemplo muito simples de usar o módulo worker_threads , nada interessante está acontecendo aqui ainda. Portanto, considere outro exemplo.

Exemplo No. 2


Considere um exemplo no qual, em primeiro lugar, executaremos alguns cálculos "pesados" e, em segundo lugar, faremos algo assíncrono no thread principal.

 const { Worker, isMainThread, parentPort, workerData } = require('worker_threads'); const request = require("request"); if(isMainThread) {   console.log("This is the main thread")   let w = new Worker(__filename, {workerData: null});   w.on('message', (msg) => { //  !       console.log("First value is: ", msg.val);       console.log("Took: ", (msg.timeDiff / 1000), " seconds");   })   w.on('error', console.error);   w.on('exit', (code) => {       if(code != 0)           console.error(new Error(`Worker stopped with exit code ${code}`))   });   request.get('http://www.google.com', (err, resp) => {       if(err) {           return console.error(err);       }       console.log("Total bytes received: ", resp.body.length);   }) } else { //    function random(min, max) {       return Math.random() * (max - min) + min   }   const sorter = require("./list-sorter");   const start = Date.now()   let bigList = Array(1000000).fill().map( (_) => random(1,10000))   sorter.sort(bigList);   parentPort.postMessage({ val: sorter.firstValue, timeDiff: Date.now() - start}); } 

Para executar este exemplo, preste atenção ao fato de que esse código precisa do módulo de request (ele pode ser instalado usando o npm, por exemplo, usando os npm init --yes npm install request --save e npm install request --save em um diretório vazio com o arquivo que contém o código acima npm install request --save ) e o fato de ele usar o módulo auxiliar, conectado pelo comando const sorter = require("./list-sorter"); . O arquivo deste módulo ( list-sorter.js ) deve estar no mesmo local que o arquivo descrito acima, seu código se parece com o seguinte:

 module.exports = {   firstValue: null,   sort: function(list) {       let sorted = list.sort();       this.firstValue = sorted[0]   } } 

Desta vez, estamos resolvendo simultaneamente dois problemas. Primeiro, carregamos a página inicial do google.com.br e, em segundo lugar, classificamos uma matriz gerada aleatoriamente de um milhão de números. Isso pode levar alguns segundos, o que nos dá uma grande oportunidade de ver os novos mecanismos do Node.js. em ação. Além disso, medimos aqui o tempo que o thread de trabalho leva para classificar os números, após o qual enviamos o resultado da medição (junto com o primeiro elemento da matriz classificada) para o fluxo principal, que exibe os resultados no console.


O resultado do segundo exemplo

Neste exemplo, o mais importante é demonstrar o mecanismo de troca de dados entre threads.
Os trabalhadores podem receber mensagens do thread principal graças ao método on . No código, você pode encontrar os eventos que estamos ouvindo. O evento de message é message toda vez que enviamos uma mensagem de um determinado encadeamento usando o método parentPort.postMessage . Além disso, o mesmo método pode ser usado para enviar uma mensagem para um encadeamento acessando uma instância de trabalho e recebê-las usando o objeto parentPort .

Agora, vejamos outro exemplo, muito semelhante ao que já vimos, mas desta vez prestaremos atenção especial à estrutura do projeto.

Exemplo No. 3


Como último exemplo, propomos considerar a implementação da mesma funcionalidade do exemplo anterior, mas desta vez vamos melhorar a estrutura do código, torná-lo mais limpo, trazê-lo para um formato que aprimore a conveniência de oferecer suporte a um projeto de software.

Aqui está o código para o programa principal.

 const { Worker, isMainThread, parentPort, workerData } = require('worker_threads'); const request = require("request"); function startWorker(path, cb) {   let w = new Worker(path, {workerData: null});   w.on('message', (msg) => {       cb(null, msg)   })   w.on('error', cb);   w.on('exit', (code) => {       if(code != 0)           console.error(new Error(`Worker stopped with exit code ${code}`))  });   return w; } console.log("this is the main thread") let myWorker = startWorker(__dirname + '/workerCode.js', (err, result) => {   if(err) return console.error(err);   console.log("[[Heavy computation function finished]]")   console.log("First value is: ", result.val);   console.log("Took: ", (result.timeDiff / 1000), " seconds"); }) const start = Date.now(); request.get('http://www.google.com', (err, resp) => {   if(err) {       return console.error(err);   }   console.log("Total bytes received: ", resp.body.length);   //myWorker.postMessage({finished: true, timeDiff: Date.now() - start}) //     }) 

E aqui está o código que descreve o comportamento do segmento de trabalho (no programa acima, o caminho para o arquivo com esse código é formado usando a construção __dirname + '/workerCode.js' ):

 const {  parentPort } = require('worker_threads'); function random(min, max) {   return Math.random() * (max - min) + min } const sorter = require("./list-sorter"); const start = Date.now() let bigList = Array(1000000).fill().map( (_) => random(1,10000)) /** //      : parentPort.on('message', (msg) => {   console.log("Main thread finished on: ", (msg.timeDiff / 1000), " seconds..."); }) */ sorter.sort(bigList); parentPort.postMessage({ val: sorter.firstValue, timeDiff: Date.now() - start}); 

Aqui estão os recursos deste exemplo:

  1. Agora, o código para o thread principal e para o thread de trabalho está localizado em arquivos diferentes. Isso facilita o suporte e a expansão do projeto.
  2. A função startWorker retorna uma nova instância do trabalhador, que permite, se necessário, enviar mensagens para esse trabalhador a partir do fluxo principal.
  3. Não há necessidade de verificar se o código está sendo executado no encadeamento principal (removemos a if com a verificação correspondente).
  4. O trabalhador mostra um fragmento de código comentado, demonstrando o mecanismo para receber mensagens do fluxo principal, que, dado o mecanismo de envio de mensagens já discutido, permite a troca de dados assíncrona bidirecional entre o fluxo principal e o fluxo do trabalhador.

Sumário


Neste artigo, usando exemplos práticos, examinamos os recursos do uso dos novos recursos para trabalhar com fluxos no Node.js. Se você dominou o que foi discutido aqui, significa que você está pronto para ver sua documentação e iniciar seus próprios experimentos com o módulo worker_threads . Talvez seja interessante notar que esse recurso apareceu apenas no Node.js. Embora seja experimental, com o tempo, algo em sua implementação pode mudar. Além disso, se durante seus próprios experimentos com worker_threads você encontrar erros ou descobrir que este módulo não interfere com algum recurso ausente, informe os desenvolvedores e ajude a melhorar a plataforma Node.js.

Caros leitores! O que você acha do suporte a multithreading no Node.js? Você planeja usar esse recurso em seus projetos?

Source: https://habr.com/ru/post/pt415659/


All Articles