Πριν βουτήξουμε σε εργατικά νήματα, πρέπει να περάσουμε από μερικά σημεία:

1. Το Node.js δεν είναι στην πραγματικότητα ένα νήμα

Το Node.js οριζόταν ως ένας ασύγχρονος χρόνος εκτέλεσης JavaScript με μονό νήματα. Μεμονωμένο νήμα σημαίνει ότι σε μια διαδικασία (πρόγραμμα υπό εκτέλεση), μόνο ένα σύνολο εντολών ήταν σε θέση να εκτελεστεί σε μια δεδομένη χρονική στιγμή. Στο Node.js υπάρχουν δύο τύποι νημάτων: ένας βρόχος συμβάντων (γνωστός και ως ο κύριος βρόχος, κύριο νήμα, νήμα γεγονότος, κ.λπ.) και μια ομάδα k Workers σε ένα Worker Pool (γνωστή και ως το threadpool ή libuv thread-pool) .

2. Υπάρχει ήδη πολυνήματα για εργασίες εισόδου/εξόδου

Υπάρχουν κυρίως δύο τύποι εργασιών που χειρίζεται ένας διακομιστής Node.js - εντατική I/O και εντατική CPU.

Η εντατική εφαρμογή CPU χρησιμοποιεί πολλούς κύκλους CPU. Για παράδειγμα, η κρυπτογράφηση/αποκρυπτογράφηση ή η διακωδικοποίηση βίντεο θα ήταν βαριές φορτωτές της CPU. Η εντατική εφαρμογή I/O περιμένει τον περισσότερο χρόνο για δίκτυο, σύστημα αρχείων και βάση δεδομένων. Λέγεται γενικά ότι το Node.js είναι ιδιαίτερα καλό στον εντατικό κώδικα I/O λόγω της μη αποκλειστικής αρχιτεκτονικής του I/O.

Ο βρόχος συμβάντων εκτελεί τις επανακλήσεις JavaScript που έχουν καταχωριστεί για συμβάντα και είναι επίσης υπεύθυνος για την εκπλήρωση μη αποκλειστικών ασύγχρονων αιτημάτων, όπως η είσοδος/έξοδος δικτύου. Οι εργασίες σύγχρονης εισόδου/εξόδου (όπως όλα τα API συστήματος αρχείων εκτός από το fs.FSWatcher() και αυτά που είναι ρητά σύγχρονα) παραδίδονται στο νήμα του libuv.

3. Αποφύγετε να μπλοκάρετε τον βρόχο συμβάντος και τη συγκέντρωση νημάτων

Καθώς η αρχιτεκτονική του Node χρησιμοποιεί τον βρόχο συμβάντος και έναν μικρό αριθμό νημάτων για να χειριστεί πολλούς πελάτες, εάν ένα νήμα χρειάζεται πολύ χρόνο για να εκτελέσει μια επιστροφή κλήσης (Βρόχος συμβάντος) ή μια εργασία (Εργάτης), θα αποκλειστεί. Ενώ ένα νήμα είναι αποκλεισμένο και λειτουργεί για λογαριασμό ενός πελάτη, δεν μπορεί να χειριστεί αιτήματα από άλλους πελάτες. Αυτό θα μειώσει την απόδοση του διακομιστή.

Πώς χειρίζεται το Node.js αιτήματα που περιλαμβάνουν έντονη χρήση της CPU

Υπάρχουν πολλοί τρόποι για να γίνει αυτό.

Χρήση του ενσωματωμένου Node.js Worker Pool με την ανάπτυξη πρόσθετων C++ σε αυτό

Η τεχνολογία Node γράφτηκε σε C++ κάτω από την κουκούλα, η οποία είναι πολύ γρήγορη και ισχυρή στην ανάπτυξη εφαρμογών για εργασίες χαμηλού επιπέδου ή βαρείς υπολογισμούς. Ως αποτέλεσμα, το Node είναι φιλικό προς τη C++ και επιτρέπει στους προγραμματιστές να εκτελούν κώδικα C++ σε εφαρμογές Node.js. Αυτό γίνεται με τη βοήθεια των πρόσθετων Node.

Τα πρόσθετα Node.js είναι δυναμικά συνδεδεμένα κοινόχρηστα αντικείμενα, γραμμένα σε C++. Μπορούν να φορτωθούν στο Node.js χρησιμοποιώντας τη συνάρτηση require() και να χρησιμοποιηθούν σαν να ήταν μια συνηθισμένη λειτουργική μονάδα Node.js. Χρησιμοποιούνται κυρίως για την παροχή μιας διεπαφής μεταξύ JavaScript που εκτελείται σε βιβλιοθήκες Node.js και C/C++.

Για να γράψετε πρόσθετα C++ θα πρέπει να έχετε πολύ καλή κατανόηση των εσωτερικών στοιχείων του V-8 και του libuv.

Χρήση Child Process API

Οι θυγατρικές διεργασίες ήταν το αρχικό μέσο δημιουργίας πολλαπλών νημάτων για την εφαρμογή σας και ήταν διαθέσιμες από την έκδοση 0.10. Αυτό επιτεύχθηκε με τη δημιουργία μιας διαδικασίας κόμβου για κάθε πρόσθετο νήμα που θέλετε να δημιουργήσετε.

Χρησιμοποιεί την ενότητα child_process η οποία παρέχει τη δυνατότητα δημιουργίας νέων διεργασιών που έχουν τη δική τους μνήμη. Η επικοινωνία μεταξύ αυτών των διεργασιών πραγματοποιείται μέσω IPC (inter-process communication) που παρέχεται από το λειτουργικό σύστημα.

Χρήση του API συμπλέγματος

Το Clustering, το οποίο είναι μια σταθερή έκδοση περίπου από την έκδοση 4, μας επιτρέπει να απλοποιήσουμε τη δημιουργία και τη διαχείριση των Child Processes που χρησιμοποιεί το child_process.fork() για την αυτόματη διακλάδωση των διεργασιών. Είναι χτισμένο πάνω στη μονάδα child_process.

Η δημιουργία θυγατρικής διαδικασίας είναι δαπανηρή όσον αφορά τους πόρους του συστήματος, καθώς απαιτείται ξεχωριστή μνήμη για κάθε θυγατρική διαδικασία. Κάτι ακόμα είναι ότι μόνο η επικοινωνία γονέα με παιδί είναι δυνατή και δεν υπάρχει επικοινωνία από παιδί με παιδί.

Νήματα εργασίας

Τι είναι τα νήματα εργασίας;

Τα νήματα εργαζομένων παρέχουν έναν μηχανισμό δημιουργίας νέων νημάτων για τη διαχείριση εργασιών έντασης CPU σε ένα πρόγραμμα Node. Ένας εργαζόμενος είναι ένα κομμάτι κώδικα είτε μέσα στο ίδιο αρχείο είτε ως ξεχωριστό σενάριο. Μπορούμε να μεταφορτώσουμε οποιεσδήποτε «ακριβές» εργασίες (ειδικά με ένταση CPU) στο νήμα εργαζομένων και να τις εκτελέσουμε ξεχωριστά χωρίς να μπλοκάρουμε τον βρόχο συμβάντος.

Αυτό επιτυγχάνεται χρησιμοποιώντας μια ενότητα που ονομάζεται worker_threads. Αυτή η ενότητα κυκλοφόρησε με το Node.js v10.x και έγινε σταθερό με το v12.x.

Πώς λειτουργούν;

Τα νήματα Worker έχουν μία διαδικασία και πολλαπλά νήματα. Στην ιδανική περίπτωση, ο αριθμός των νημάτων που δημιουργούνται θα πρέπει να είναι ίσος με τον αριθμό των πυρήνων της CPU στο σύστημα.

Κάθε εργαζόμενος είναι απομονωμένος από τους άλλους. Αυτό σημαίνει ότι κάθε εργαζόμενος εκτελεί τον κώδικα JavaScript του εντελώς απομονωμένος από άλλους εργαζόμενους. Αυτή η απομόνωση επιτυγχάνεται με V8 Isolate — χρόνους εκτέλεσης V8 που δημιουργούνται από τη μηχανή V8 του Chrome στην οποία εκτελείται η εφαρμογή Node. Εξαιτίας αυτού, κάθε εργαζόμενος θα έχει το δικό του αντίγραφο του κινητήρα V8, του βρόχου συμβάντων και της παρουσίας κόμβου που είναι ανεξάρτητο από τους βρόχους συμβάντων του άλλου εργαζόμενου και του γονέα που εκτελούνται στην ίδια διαδικασία. Αυτό έχει ως αποτέλεσμα την παράλληλη εκτέλεση κώδικα JavaScript.

Σε αντίθεση με τις θυγατρικές διεργασίες, τα νήματα μπορούν να μοιράζονται τη μνήμη χρησιμοποιώντας αντικείμενα ArrayBuffer, SharedArrayBuffer και Atomics. Το ArrayBuffer χρησιμοποιείται για τη μεταφορά μνήμης από το ένα νήμα στο άλλο. Η κοινή χρήση μνήμης μεταξύ δύο νημάτων χρησιμοποιώντας το SharedArrayBuffer θα μπορούσε να οδηγήσει σε συνθήκες κούρσας στο πρόγραμμα. Οι συνθήκες φυλής προκύπτουν όταν δύο νήματα προσπαθούν να διαβάσουν και να γράψουν στην ίδια θέση μνήμης την ίδια στιγμή. Χρησιμοποιώντας το Atomics, μπορούμε να αποτρέψουμε την εμφάνιση συνθηκών κούρσας επειδή, τώρα, μόνο ένα νήμα έχει πρόσβαση στην κοινόχρηστη μνήμη σε μια δεδομένη στιγμή.

Μπορούμε να δούμε πώς τα νήματα εργαζομένων είναι καλά στο χειρισμό αιτημάτων έντασης CPU στο ακόλουθο παράδειγμα. Εδώ, ένας γρήγορος διακομιστής που εκτελεί μια μακρά υπολογιστική εργασία με δύο τρόπους.

Στο πρώτο σενάριο, η διεύθυνση URL αιτήματος λήγει σε '/one' εκτελείται στο κύριο νήμα. Τα νήματα εργασίας δεν χρησιμοποιούνται εδώ. Το κύριο νήμα χειρίζεται όλα τα αιτήματα προς τον διακομιστή. Εάν υπάρχουν 10 ταυτόχρονα αιτήματα, το καθένα θα εξυπηρετείται το ένα μετά το άλλο. Έτσι, όταν ένα αίτημα είναι υπό επεξεργασία, όλα τα υπόλοιπα αιτήματα πρέπει να περιμένουν, διαφορετικά μπορούμε να πούμε ότι ο βρόχος συμβάντων του Node.js έχει αποκλειστεί. Αυτό δεν είναι επιθυμητό.

Στη δεύτερη περίπτωση, στην οποία η διεύθυνση URL τελειώνει σε '/two', χρησιμοποιούμε worker_threads και δημιουργούμε έναν νέο εργαζόμενο χρησιμοποιώντας την κλάση Worker για να εκτελέσουμε την εργασία εντατική CPU. Τα νήματα εργασίας θα κάνουν τη δουλειά τους χωρίς να μπλοκάρουν το κύριο νήμα. Μετά την ολοκλήρωση της εργασίας, μπορούν να επικοινωνήσουν με τον γονέα χρησιμοποιώντας το postMessage() και ο γονέας μπορεί να ακούσει ένα συμβάν "μήνυμα".

Η επίσημη τεκμηρίωση του Node.js παρέχει λεπτομέρειες API σε worker_threads.

const express = require('express')
const { Worker} = require('worker_threads');
const app = express()
app.get('/one', (req, res) => {
  console.log(`Starting the long computation`)
  let sum = 0;
  for (let i = 0; i <= 1e9; i++){
     sum += i;
  }
  console.log(`Done with the computation`)
  res.send({sum:sum})
})
app.get('/two', (req, res) => {
  console.log(`Going to create a new worker thread`)
  const worker = new Worker("./myWorker.js")
  worker.on('message', (msg) => {
    console.log(`Worker: ${msg.result}`);
    res.send({sum:msg.result})
  });
  console.log(`Other works in the main thread`)
})
app.listen(9000, () => {
  console.log(`listening on port 9000`)
})

myWorker.js

const {parentPort} = require("worker_threads")
console.log(`Busy in worker thread`)
let sum = 0;
for (let i = 0; i <= 1e9; i++){
   sum += i;
}
parentPort.postMessage({
  result: sum
})

Επικοινωνία μεταξύ των νημάτων

Μπορούμε να χρησιμοποιήσουμε την παράμετρο workerData για να στείλουμε δεδομένα στον νεοδημιουργημένο εργαζόμενο. Μπορεί να είναι οποιαδήποτε τιμή που μπορεί να κλωνοποιηθεί από τον αλγόριθμο δομημένου κλώνου — οι εργαζόμενοι λαμβάνουν κλωνοποιημένη τιμή.

Για την επικοινωνία μεταξύ ενός γονικού και ενός νήματος εργαζόμενου, χρησιμοποιούνται παρουσίες κλάσης MessagePort και Worker. Ένα γονικό νήμα στέλνει δεδομένα χρησιμοποιώντας το worker.postMessage() και αυτά τα δεδομένα θα είναι διαθέσιμα στον εργαζόμενο χρησιμοποιώντας το parentPort.on('message'). Επίσης, ένας εργαζόμενος μπορεί να στείλει δεδομένα χρησιμοποιώντας το parentPort.postMessage() και θα είναι διαθέσιμο στο γονικό νήμα χρησιμοποιώντας το worker.on('message'). Εδώ, το parentPortείναι μια παρουσία του MessagePort και το worker είναι ένα παράδειγμα της κλάσης Worker.

Μπορούμε να δημιουργήσουμε τα λιμάνια μας εάν βρεθούμε σε μια κατάσταση που απαιτεί μια πιο περίπλοκη λύση. Χρησιμοποιούμε την κλάση MessageChannel για αυτό, η οποία αντιπροσωπεύει ένα ασύγχρονο, αμφίδρομο κανάλι επικοινωνίας. Αυτή η κλάση έχει μόνο δύο ιδιότητες - port1 και port2 και οι δύο περιπτώσεις του MessagePort. Κάθε θύρα αντιπροσωπεύει το ένα άκρο του καναλιού επικοινωνίας.

Παρουσίαση της ομάδας εργαζομένων

Όταν εργάζεστε σε ένα πραγματικό σενάριο, θα πρέπει να διαχειρίζεστε τη δική σας ομάδα εργαζομένων και να τους διαχειρίζεστε, έτσι ώστε ο αριθμός των νημάτων που εκτελούνται να μην υπερβαίνει τον αριθμό των CPU. Επίσης, η δημιουργία ενός νήματος είναι εύκολη, αλλά η δυναμική διαχείριση πολλών νημάτων είναι δύσκολη. Για αυτό, μπορούμε να χρησιμοποιήσουμε μια λειτουργική μονάδα τρίτου κατασκευαστή, γνωστή ως ομάδα εργασίας.

Το workool προσφέρει έναν εύκολο τρόπο δημιουργίας μιας ομάδας εργαζομένων τόσο για δυναμική εκφόρτωση υπολογισμών όσο και για διαχείριση μιας ομάδας αφοσιωμένων εργαζομένων. Το workerpool ουσιαστικά υλοποιεί ένα μοτίβο συγκέντρωσης νημάτων. Υπάρχει μια ομάδα εργαζομένων για την εκτέλεση εργασιών. Οι νέες εργασίες τοποθετούνται σε μια ουρά. Ένας εργαζόμενος εκτελεί μία εργασία κάθε φορά και μόλις τελειώσει, επιλέγει μια νέα εργασία από την ουρά. Οι εργαζόμενοι μπορούν να έχουν πρόσβαση μέσω ενός φυσικού διακομιστή μεσολάβησης που βασίζεται σε υποσχέσεις, σαν να είναι διαθέσιμοι απευθείας στην κύρια εφαρμογή.

Είναι βασικά ένας διαχειριστής νημάτων για το Node.js, που υποστηρίζει Worker Threads, Child Processes και Web Workers για υλοποιήσεις που βασίζονται σε πρόγραμμα περιήγησης.

Μπορούμε να φορτώσουμε τη μονάδα workpool χρησιμοποιώντας

const workerpool = require(‘workerpool’)

Για να δημιουργήσετε μια ομάδα εργαζομένων, η ομάδα εργασίας προσφέρει μια ομάδα συνάρτησης () και για να δημιουργήσετε μια ομάδα εργασίας, προσφέρει τη συνάρτηση worker().

workerpool.pool([script: string] [, options: Object]) : Pool

Όταν παρέχεται ένα όρισμα script, το παρεχόμενο σενάριο θα ξεκινήσει ως αποκλειστικός εργαζόμενος. Το script πρέπει να είναι μια απόλυτη διαδρομή αρχείου όπως __dirname + '/myWorkerPool.js'. Μπορούμε επίσης να ορίσουμε ελάχιστο και μέγιστο αριθμό εργαζομένων και τύπο εργαζομένου στο όρισμα επιλογές. Από προεπιλογή, ο μέγιστος αριθμός εργαζομένων θα είναι ο αριθμός των CPU μείον ένα. Μόλις δημιουργηθεί μια ομάδα εργαζομένων, οι συναρτήσεις θα πρέπει να εγγραφούν στον εργαζόμενο. Οι καταχωρημένες λειτουργίες θα είναι διαθέσιμες μέσω της ομάδας εργαζομένων. Τώρα, η μέθοδος pool.exec() εκτελεί αυτή τη συνάρτηση σε έναν εργαζόμενο και επιστρέφει ένα Promise.

Το ίδιο παράδειγμα που υλοποιήθηκε με χρήση της ομάδας εργασίας φαίνεται παρακάτω.

myWorkerPool.js

const workerpool = require('workerpool');
function getSum() {
  let sum = 0;
  for (let i = 0; i <= 1e9; i++){
    sum += i;
  }
  return sum
}
// create a worker and register public functions
workerpool.worker({
   getSum: getSum
});

index.js

const workerpool = require('workerpool');
const express = require('express')
const app = express()
app.get('/', (req, res) => {
  const pool = workerpool.pool(__dirname + '/myWorkerPool.js');
  pool.exec('getSum')
  .then(function (result) {
     res.send({result:result})
  })
  .catch(function (err) {
     console.error(err);
  })
  .then(function () {
    pool.terminate(); // terminate all workers when done
   });
})
app.listen(7000, () => {
  console.log("listening on port 7000")
})

συμπέρασμα

Τα νήματα εργαζομένων είναι η πρόσφατη μέθοδος για την πραγματοποίηση λειτουργιών με ένταση CPU. Δεν θα βοηθήσουν πολύ με την εργασία με ένταση I/O. Οι ενσωματωμένες ασύγχρονες λειτουργίες εισόδου/εξόδου του Node.js είναι πιο αποτελεσματικές από ό,τι οι Workers. Επίσης, σε πραγματικά σενάρια εργασίας, αντί να δημιουργούμε εργάτες κάθε φορά που χρειάζεται να χρησιμοποιήσετε, μπορούμε να δημιουργήσουμε μια ομάδα εργαζομένων χρησιμοποιώντας τη μονάδα workpool.