ABONAMENTE VIDEO REDACȚIA
RO
EN
Numărul 148 Numărul 147 Numărul 146 Numărul 145 Numărul 144 Numărul 143 Numărul 142 Numărul 141 Numărul 140 Numărul 139 Numărul 138 Numărul 137 Numărul 136 Numărul 135 Numărul 134 Numărul 133 Numărul 132 Numărul 131 Numărul 130 Numărul 129 Numărul 128 Numărul 127 Numărul 126 Numărul 125 Numărul 124 Numărul 123 Numărul 122 Numărul 121 Numărul 120 Numărul 119 Numărul 118 Numărul 117 Numărul 116 Numărul 115 Numărul 114 Numărul 113 Numărul 112 Numărul 111 Numărul 110 Numărul 109 Numărul 108 Numărul 107 Numărul 106 Numărul 105 Numărul 104 Numărul 103 Numărul 102 Numărul 101 Numărul 100 Numărul 99 Numărul 98 Numărul 97 Numărul 96 Numărul 95 Numărul 94 Numărul 93 Numărul 92 Numărul 91 Numărul 90 Numărul 89 Numărul 88 Numărul 87 Numărul 86 Numărul 85 Numărul 84 Numărul 83 Numărul 82 Numărul 81 Numărul 80 Numărul 79 Numărul 78 Numărul 77 Numărul 76 Numărul 75 Numărul 74 Numărul 73 Numărul 72 Numărul 71 Numărul 70 Numărul 69 Numărul 68 Numărul 67 Numărul 66 Numărul 65 Numărul 64 Numărul 63 Numărul 62 Numărul 61 Numărul 60 Numărul 59 Numărul 58 Numărul 57 Numărul 56 Numărul 55 Numărul 54 Numărul 53 Numărul 52 Numărul 51 Numărul 50 Numărul 49 Numărul 48 Numărul 47 Numărul 46 Numărul 45 Numărul 44 Numărul 43 Numărul 42 Numărul 41 Numărul 40 Numărul 39 Numărul 38 Numărul 37 Numărul 36 Numărul 35 Numărul 34 Numărul 33 Numărul 32 Numărul 31 Numărul 30 Numărul 29 Numărul 28 Numărul 27 Numărul 26 Numărul 25 Numărul 24 Numărul 23 Numărul 22 Numărul 21 Numărul 20 Numărul 19 Numărul 18 Numărul 17 Numărul 16 Numărul 15 Numărul 14 Numărul 13 Numărul 12 Numărul 11 Numărul 10 Numărul 9 Numărul 8 Numărul 7 Numărul 6 Numărul 5 Numărul 4 Numărul 3 Numărul 2 Numărul 1
×
▼ LISTĂ EDIȚII ▼
Numărul 90
Abonament PDF

Node.js Worker Threads

Alexandru Beșe
Module Lead @ 3Pillar Global



PROGRAMARE

Unii dintre noi sunt de părere că javascript nu este un limbaj de programare, alții îi apreciază foarte mult flexibilitatea, dar cei mai mulți cred că popularitatea masivă, de care s-a bucurat node în ultimii ani, este înscrisă pe un trend ascendent.

La finalul lunii octombrie s-a lansat versiunea 12 Long Term Support (4) a cunoscutului runtime. După multe teste și modificări ale acestei versiuni, se lansează oficial și production ready, modulul de worker threads, care le va permite programatorilor să scrie aplicații multi-threaded (7) în Node.js. În acest fel, îi facilitează intrarea pe un segment de piață, unde în trecut n-a avut nicio șansă în fața giganților Java, .NET ș.a.m.d.

Node.js este single-threaded

Un număr semnificativ de programatori crede că node este single-threaded. Presupunerea vine de la faptul că javascript este single-threaded. Dar din punctul meu de vedere, node este parțial multi-threaded. În favoarea acestei afirmații sunt argumentele de mai jos.

Dacă intrăm pe pagina de GitHub a proiectului și privim tehnologiile folosite împărțite pe procentaje, în momentul scrierii acestui articol erau: Javascript 51.5%, C++ 28.8, Python 14.5%, C 2.8%, plus altele. Foarte multe părți scrise în C++. De ce?

Există multe module precum: filesystem (5), I/O (folosind libuv), crypto ș.a. care sunt implementate în C++. Când apelezi o metodă de node, ea apelează implementarea metodei din C++ prin ajutorul unei funcții internalBinding.

Un lucru important de reținut este că atunci când folosim aceste module, instrucțiunile lor se execută într-un context multi-threaded; în schimb, codul javascript (if-urile, while-urile ș.a.m.d) se execută într-adevăr într-o manieră single-threaded pe firul de execuție principal.

În majoritatea cazurilor implementării unui API pentru o aplicație scrisă în React, Angular sau Vue, totul este modularizat pe rute, modele și controlere. Așadar, nu avem un codebase foarte mare de executat. Pe lângă aceasta, primim puncte bonus la performanță, dacă aplicația noastră folosește pachete care au implementări multi-threaded în C++.

Probabil acesta este unul dintre motivele pentru care node a prins atât de repede la public. Adevăratul motiv a fost de fapt avantajul de a scrie frontend și backend în același limbaj, fără context switching. Însă aș vrea să vorbim puțin despre elefantul din cameră...

Precum bine știm, codul javascript se execută pe procesor, iar operațiile cpu-intensive nu au fost niciodată punctul forte al lui node. Una din soluțiile acestei probleme este crearea unui process pool care să proceseze în background aceste operații, aceasta dacă utilizatorul nu are nevoie imediată de rezultatul operației. Dacă da? Să spunem doar că se complică puțin lucrurile.

Modulul worker threads

Cu introducerea modulului worker threads se deschide o nouă lume plină de fire de execuție pe A.I (8), I.o.T (9), machine learning (10) și operații cpu-intensive.

Decizia folosirii lui este în mâna programatorului. Cu ajutorul unui canal de comunicare, modulul permite să trimită execuția unei bucăți de instrucțiuni pe un alt fir de execuție, evitând blocarea firului de execuție principal (cel care ne face aplicația single-threaded). După finalizarea execuției, firul de execuție principal este notificat pe canalul de comunicare de faptul că operația s-a terminat, primind rezultatul ei.

Sumarul ofertei ecosistemului javascript este reprezentat de:

Gestionarea firelor de execuție

Un procesor are un număr fizic limitat de fire de execuție, de obicei, dublul numărului de nuclee.

Nu insistăm asupra detaliilor despre felul în care sistemul de operare prioritizează instrucțiunile pe aceste fire de execuție, însă trebuie să fim atenți la modul în care ne alocăm resursele pe ele, pentru că acest lucru poate să ne afecteze performanța aplicației.

Dacă în trecut foloseam pm2 (6) ca să pornim câte o instanță de node pentru fiecare fir de execuție disponibil, acum trebuie să privim diferit problema.

Performanța unui server de node scade în momentul în care ne creăm o instanță pentru fiecare fir de execuție disponibil, din cauza proceselor de node care rulează pe ele, forțăm semaforul sistemului de operare să ne ruleze bucăți de instrucțiuni, în funcție de disponibilitatea redusă a firelor de execuție, pierzând avantajul multi-threadingului oferit de pachetele scrise în C++.

Am rulat câteva teste pentru a evidenția acest lucru:

Pentru 100.000 de request-uri cu o concurență de 100 folosind apache benchmark:

ab -n 100000 -c 100 -k -H
"Accept-Encoding: gzip, deflate"
http://localhost:3000/users/5d99e41af25745db0265bb88

Am rulat instrucțiunea de mai sus în două situații: una în care am pornit câte o aplicație de node pentru fiecare fir de execuție, cealaltă, în care am lăsat jumătate din firele de execuție libere. Am rulat fiecare caz de cinci ori, salvând rezultatele cele mai bune.

Precum am anticipat, deoarece modulul de http/network e multi-threaded, pornirea unei aplicații de node pentru fiecare thread ne va încetini atât aplicația cât și timpii de răspuns pentru o rută simplă GET, care îmi aduce informațiile unui utilizator. Din cauza disponibilității reduse a firelor de execuție, modulul multi-threaded este încetinit de către semaforul sistemului de operare.

Pentru a rezolva această problemă, trebuie să ne gândim cum alocăm resursele și să ne asigurăm că avem fire de execuție cu disponibilitate mare pentru a spori performanțele aplicației noastre.

Această decizie trebuie luată în funcție de arhitectura procesorului pe care rulează aplicația noastră, dar pentru a ne asigura că rulează performant ar trebui să alocăm cel puțin:

Implementare

Am creat un git repository (3) cu o implementare simplistă a unei funcții cpu-intensive care rulează pe un fir de execuție diferit. La finalizarea operației, notifică firul de execuție principal cu rezultatul.

Părțile importante sunt:

const {
    Worker, MessageChannel, isMainThread, parentPort
} = require('worker_threads');

if (isMainThread) { 
  // dacă suntem pe firul de execuție principal

  const worker = new Worker(__filename); 
  /* instanțiem un worker nou avand ca și cod sursă 
     fișierul curent */

  const subChannel = new MessageChannel();  
  /* un canal de communicare între firul principal de   
     execuție și workerul creat */

  /* trimitem mesaj pe canalul workerului cu un 
     obiect care conține portul lui propriu ca să ne 
     fie mai ușor să răspundem înapoi */
  worker.postMessage({ myPort: subChannel.port1 },  
    [subChannel.port1]); 
  console.log("Lets start making some work on" +
    " a different thread!");

  /* ascultăm după răspunsul firului de 
execuție secundar (workerului)  */
  subChannel.port2.on('message', (value) => {
    console.log('received:', value); 
    // primim rezultatul operației de la firul de 
    // execuție secundar
  });

  // putem executa orice altceva pe firul de execuție 
  // principal 

  } else {
  /* aici ajunge workerul la instrucțiunea new 
     Worker(__filename) tot aici putem executa orice  
     dorim, separat de firul de execuție principal */

  const result = myIntensiveOperation(); 
  // operația 

  /* când primim un mesaj de la firul de execuție 
     părinte (în acest caz, principal) */

  parentPort.once('message', (message) => { 
  /* după ce am terminat de procesat operația 
    transmitem rezultatul ei înapoi firului de 
    execuție principal care decide ce să facă cu ea*/

  message.myPort.postMessage(result); 
  /* folosim obiectul transmis din firul de execuție 
     principal { myPort: subChannel.port1 } pentru a transmite 
rezultatul inapoi */
        message.myPort.close();
    });

}

Câteva articole

Citisem un articol (1) unde era prezentat un proiect care se baza pe funcții lambda în AWS. Folosirea worker threads a redus timpii de procesare, iar același codebase se execută cu 75% mai repede față de varianta fără worker threads, salvând timp de procesare în AWS. În final, costurile s-au redus cu 75%.

Un alt articol (2) prezintă dezavantajul folosirii unui singur worker. Canalul de comunicare și trimiterea mesajelor de pe un fir de execuție pe celălalt este scumpă, din acest motiv este indicat să distribuim o procedură cpu-intensive pe mai multe fire de execuție pentru a putea profita de avantajele acestui modul.

Articolul mai prezintă o implementare folosind modulul "worker-threads-pool" pentru gestionarea firelor de execuție disponibile fizic, plus câteva teste pe un set mare de date.

Cu cât avem de procesat mai multe date, cu atât devine mai mare avantajul folosirii acestui modul.

În concluzie, viitorul sună bine pentru node.

  1. http://jamesthom.as/blog/2019/05/08/node-dot-js-worker-threads-with-serverless-functions/

  2. https://medium.com/lazy-engineering/node-worker-threads-b57a32d84845

  3. https://github.com/alexandrubese/node-workerthreads

  4. https://en.wikipedia.org/wiki/Long-term_support

  5. https://en.wikipedia.org/wiki/File_system

  6. https://pm2.keymetrics.io/

  7. https://en.wikipedia.org/wiki/Multithreading_(computer_architecture)

  8. https://en.wikipedia.org/wiki/Artificial_intelligence

  9. https://en.wikipedia.org/wiki/Internet_of_things

  10. https://en.wikipedia.org/wiki/Machine_learning

LANSAREA NUMĂRULUI 149

Marți, 26 Octombrie, ora 18:00

sediul Cognizant

Facebook Meetup StreamEvent YouTube

NUMĂRUL 147 - Automotive

Sponsori

  • Accenture
  • BT Code Crafters
  • Accesa
  • Bosch
  • Betfair
  • MHP
  • BoatyardX
  • .msg systems
  • P3 group
  • Ing Hubs
  • Cognizant Softvision
  • Colors in projects