Il concetto di concorrenza in applicazioni Node.JS

Come gestire la concorrenza in applicazioni multitask utilizzando gli strumenti messi a disposizione da Node.JS

Node.js è un nuovo approccio alla programmazione server side, basato sull’interprete V8 di Google per Chrome. Il vantaggio è quello di permettere la realizzazione di script lato server usando un linguaggio di programmazione molto simile a JavaScript: ciò consente a chi ha un background specializzato nel frontend di affrontare anche gli aspetti caratteristici del backend.
Dato questo approccio, è lecito avere alcune perplessità sulla gestione delle attività concorrenti sul server. I linguaggi di programmazione tradizionali di solito gestiscono le operazioni concorrenti aprendo un nuovo thread per ogni richiesta. Questo modo di lavorare è detto programmazione multithreading. Per chi non conoscesse l’argomento, riassumiamo brevemente i concetti fondamentali.
Da almeno trent’anni l’informatica supporta il multitasking, che significa gestire l’esecuzione di più processi simultaneamente. Ad esempio ciò permette di usare contemporaneamente un programma di videoscrittura, una chat e il browser, di solito associati ciascuno ad una finestra del sistema operativo. Questa divisione delle attività significa che abbiamo un processo per ogni programma in esecuzione. Il concetto di thread è molto simile, perché applica lo stesso approccio ad un livello inferiore. Ciò significa che un particolare processo può eseguire diverse azioni contemporaneamente. Ad esempio, se consideriamo un software di videoscrittura come Office o OpenOffice, possiamo mandare in stampa un documento mentre stiamo lavorando ad un altro testo. Così come il multitasking permette la divisione del lavoro in programmi, il multithreading permette la divisione dei programmi in sottoprogrammi, detti appunto “thread”.
Quanto appena detto ci basta per fare un confronto tra l’approccio tradizionale e il paradigma di Node.js. A rigore esistono alcune differenze nel modo in cui sistemi operativi distinguono tra thread e processo, ma date le finalità di questo articolo eviteremo di scendere nei dettagli. La questione che ci interessa è la seguente: se il paradigma di Node.js ricalca molto da vicino quello di JavaScript, e se JavaScript non consente la programmazione multithreading, come possiamo gestire le esigenze della programmazione concorrente con Node.js?
In realtà la domanda è mal posta. Come abbiamo visto nelle prime lezioni, Node.js è un linguaggio di programmazione orientato agli eventi, per cui la mancanza del multithreading non è un difetto né un limite del linguaggio, bensì una scelta ben ponderata. Tutto ciò che si può fare col multithreading si può anche realizzare gestendo i singoli eventi, in modo simile a quello che accade quando usiamo un browser. Un esempio di gestione delle attività concorrenti, molto semplice e immediato, lo abbiamo visto nella seconda lezione, quando abbiamo usato le funzioni JavaScript setTimeout e setInterval per mostrare il funzionamento sincrono del codice.
Data l’importanza dell’argomento vogliamo approfondire la questione. Nelle prossime pagine mostreremo in modo esplicito come sia possibile gestire diverse attività concorrenti, emulando in un certo senso la programmazione multithreading, senza in realtà parlare né di thread né di concorrenza.

Un esempio pratico

Per chiarire qual è l’approccio usato da Node.js, consideriamo le seguenti esigenze: vogliamo avere un server che faccia “qualcosa” ogni tre secondi. Non ci interessa cosa sia questo “qualcosa”: ci interessa soltanto il fatto che dobbiamo gestire un ciclo lento, che chiameremo appunto slow cycle, con un tempo fissato in tre secondi.
Oltre a questa attività, una volta al secondo vogliamo eseguire una chiamata HTTP GET verso un server remoto, e leggere alcuni dati delle risposta. Tale chiamata potrebbe essere una banale richiesta di ping per monitorare lo stato del server, un’interrogazione SOAP o una chiamata Ajax. Anche in questo caso non ci interessa sapere “cosa” deve eseguire questo ciclo, ma ci interessa soltanto che tale attività venga eseguita in parallelo alla precedente, con un timing di un secondo. Per semplicità, nell’implementare questa specifica, ci limiteremo a leggere un valore dell’header della risposta HTTP.
Consideriamo infine la necessità di rispondere alle normali richieste che arrivano sul server da parte degli utenti, anche queste sotto forma di una richiesta HTTP. Quest’attività non sarà a scadenza fissa, perché non possiamo assumere che gli utenti avanzino una richiesta ogni tot tempo. Sappiamo soltanto che riceveremo delle richieste, cioè in maniera asincrona rispetto alle altre attività.

Il codice Node.js che soddisfa tutte queste esigenze è il seguente:

dove notiamo l’introduzione della chiamata http.get(), che è l’unica novità rispetto alle lezioni precedenti. A parte questo il codice non dovrebbe necessitare di alcun commento di natura sintattica, perché tutte le funzioni utilizzate sono già state discusse nelle lezioni precedenti.

Approfondimenti

Per eseguire il codice di pagina precedente basta copiare ed incollare il sorgente all’interno di un file, che per comodità chiameremo server.js. Andremo poi ad eseguirlo digitando il comando node server.js. Il risultato dovrebbe essere seguente:

multithreading_01
Figura 1 – Esecuzione del server

Dalla finestra di esecuzione si vede come le attività vengano gestite simultaneamente, nel rispetto del timing previsto, assieme alle richieste che giungono sul server HTTP. Come già detto, questo risultato non ha nulla che vedere con la programmazione multithreading, perché stiamo usando un approccio completamente diverso, detto event-driven. Il risultato è comunque più che soddisfacente: procediamo quindi al confronto con l’approccio della normale programmazione multithreading.
Dal punto di vista delle performance l’approccio di Node.js, almeno per server di piccole dimensioni, dovrebbe essere vantaggioso. L’orientamento agli eventi permette di risparmiare parecchio tempo macchina, perché le varie funzioni vengono attivate soltanto quando serve. Al contrario, nella programmazione multithreading, di solito i vari thread vengono messi “a dormire” tra una richiesta all’altra, ma di fatto continuano a restare “idle” e sono quindi presenti nell’elenco delle cose da gestire da parte del sistema operativo.
Le cose cambiano quando passiamo a server di grandi dimensioni. Se il numero di richieste è molto elevato, o le attività concorrenti numerose, l’approccio offerto da Node.js potrebbe non sembrare adeguato ad un servizio professionale. A questo punto entra in gioco il significato del nome Node.js. I creatori di questo linguaggio non hanno scelto il nome a caso. L’idea è quella di creare un server composto da moduli di piccole dimensioni, che possono essere considerati dei “mini server” autonomi nel loro funzionamento. Ciascuno di essi va considerato come un nodo del sistema, da cui appunto il nome Node.js.
Affinché l’architettura funzioni è necessario che questi “mini server” dialoghino tra loro, scambiandosi tutte le formazioni necessarie. Questo spiega perché la comunicazione e lo scambio di dati è un aspetto molto importante se vogliamo lavorare con Node.js. Ad esempio, la chat via TCP/IP che abbiamo visto nella quinta lezione non è un’applicazione frivola come sembra, ma al contrario presenta un primo esempio di comunicazione tra nodi differenti. Per sfruttare a fondo la flessibilità e la scalabilità offerta da questo approccio, nelle prossime lezioni approfondiremo l’argomento della comunicazione dei diversi componenti.

Facci sapere cosa ne pensi!

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *