Processi e thread: lavorare da soli o usare la tecnica dei cloni

Un sapere da colloquio, recitato come una presentazione di un minuto

«Qual è la differenza tra un processo e un thread?»

Quando cercavo il mio primo lavoro, questa domanda compariva sempre nella “Top 10 delle domande più frequenti ai colloqui”. E io rispondevo come una macchina.

«Un processo è un programma in esecuzione, mentre un thread è un flusso di esecuzione all’interno di un processo. I processi non condividono le risorse, i thread invece sì.»

Il selezionatore ha annuito, e io mi sono illuso di aver capito alla perfezione il concetto. Ma finché sul lavoro non mi sono scontrato con i “problemi di concorrenza” (Concurrency Issue), non avevo la minima idea di quanto fosse davvero spaventoso ciò che quella frase sottintendeva.

«Perché il contatore delle visualizzazioni dovrebbe salire di 100 e invece sale solo di 98?» «Perché il sito si blocca quando qualcuno avvia un download in Excel?»

Il mio codice era perfetto finché girava da solo, ma appena sono arrivati più utenti contemporaneamente è diventato un disastro. Pensavo che bastasse aggiungere “lavoratori” per risolvere tutto. Non sapevo che più lavoratori aggiungi, più il costo per gestirli (Context Switching) cresce in modo esponenziale.

Da soli si lavora in pace, ma quando si è in tanti scoppia la guerra.

Fabbriche e lavoratori nel centro logistico digitale

Torniamo al nostro universo del “centro logistico digitale”. Eseguire un programma su un computer è come aprire una “fabbrica” (Factory) all’interno del centro logistico.

1. Processo (Process): una fabbrica indipendente

  • Definizione: un’“unità di lavoro indipendente” a cui il sistema operativo (il direttore della fabbrica) assegna delle risorse.
  • Caratteristiche: ognuno ha il proprio edificio. Se la fabbrica accanto crolla (errore), la mia resta al sicuro.
  • Costo: per costruire una fabbrica serve un terreno (memoria) e bisogna anche registrare l’attività (PID). È “costosa e pesante”.
  • Condivisione: in linea di principio non puoi prendere la merce della fabbrica accanto. (Serve una comunicazione complessa chiamata IPC.)

2. Thread: i lavoratori dentro la fabbrica

  • Definizione: il “lavoratore” che svolge concretamente il lavoro dentro il processo (la fabbrica).
  • Caratteristiche: in una sola fabbrica possono lavorare più operai (multithread).
  • Costo: assumere un nuovo lavoratore è molto più “economico e leggero” che costruire una nuova fabbrica.
  • Condivisione: ecco il punto chiave. I lavoratori usano in comune la “mensa (Heap) e la sala relax (Data)”.
Un thread ha allo stesso tempo uno “spazio condiviso” (Heap) e uno “spazio indipendente” (Stack).

[Code Verification] La tragedia della condivisione (problemi di concorrenza)

«I thread condividono le risorse.» Al colloquio suonava come un pregio, ma nel lavoro vero può essere il seme di una “catastrofe”.

Ti ricordi l’heap (Heap) di cui abbiamo parlato la volta scorsa? I thread condividono quest’area. In altre parole, il thread B può arrivare e sovrascrivere i dati su cui sta lavorando il thread A.

Questa si chiama “corsa critica” (Race Condition). Verifichiamolo con il codice.

public class RaceConditionTest {
    static int count = 0; // variabile condivisa, salvata nell'heap (Heap)

    public static void main(String[] args) throws InterruptedException {
        // assumiamo due lavoratori (thread)
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) count++;
        });
        
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) count++;
        });

        t1.start(); // al lavoro!
        t2.start(); // al lavoro!
        
        t1.join(); // aspetta che finisca il turno
        t2.join(); // aspetta che finisca il turno
        
        System.out.println("Risultato finale: " + count);
    }
}

Risultato atteso: due lavoratori hanno sommato 10.000 volte ciascuno, quindi dovrebbe uscire 20,000.

Risultato reale: 15,482, 18,931… ogni esecuzione dà un valore diverso, e 20.000 non esce mai.

Il motivo:

  1. Il thread A legge count(100) dall’heap e lo porta nello stack.
  2. Nel frattempo anche il thread B legge count(100) dall’heap.
  3. A lo porta a 101 e lo salva nell’heap.
  4. Anche B (visto che aveva letto 100) lo porta a 101 e lo salva nell’heap.
  5. Abbiamo sommato due volte, ma il risultato è come se avessimo sommato una volta sola.

Ecco la vera identità del bug per cui, nel lavoro reale, «ogni tanto i dati spariscono nel nulla». È la tragedia di lavoratori che mettono mano allo stesso libro contabile senza parlarsi.

Se paragonassimo questa situazione a un database (DB), sarebbe identica a “più query lanciate contemporaneamente senza una transazione (Transaction)”. Si modifica il saldo di un conto nello stesso istante senza alcuna protezione (Lock), ed ecco l’incidente terribile: i soldi evaporano. Così come nel DB proteggiamo i dati con ROLLBACK o COMMIT, anche a livello di codice serve per forza un meccanismo di blocco come Synchronized.

Il trade-off nella pratica: multiprocesso vs multithread

E allora, nel lavoro reale, quando conviene usare l’uno e quando l’altro?

1. La scelta del browser Chrome: multiprocesso

Con il vecchio Internet Explorer, se una scheda si bloccava si chiudeva l’intero browser (approccio multithread). Chrome invece avvia ogni singola scheda come un “processo separato (fabbrica)”.

  • Pro: anche se una scheda smette di rispondere (la fabbrica salta in aria), le altre schede (le fabbriche accanto) restano intatte. (Stabilità)
  • Contro: divora una quantità enorme di memoria. È per questo che Chrome è diventato un “ippopotamo mangia-RAM”. (Costo)

2. La scelta dei server web (Spring, Node.js ecc.): multithread

Un server deve gestire le richieste di migliaia di persone. Se costruisse un processo (fabbrica) per ogni richiesta, esploderebbe. Per questo mette tantissimi thread (lavoratori) dentro un unico processo e gestisce tutto così.

  • Pro: usa poche risorse ed è veloce. (Efficienza)
  • Contro: se un singolo thread genera un errore che uccide il processo, muore l’intero server. Inoltre i problemi di concorrenza che abbiamo visto prima deve prevenirli lo sviluppatore in prima persona (Synchronized).
Costruire una “fortezza” sicura ma costosa, o far volare “droni” veloci ma rischiosi?

Per concludere: la tecnica dei cloni comporta delle responsabilità

Oggi abbiamo visto come lavorano i nostri lavoratori: “processi e thread”.

  • Processo: una fabbrica indipendente, sicura ma costosa.
  • Thread: un collega che condivide lo spazio, leggero e veloce ma a rischio di scontri.

Ormai avrai capito che “problemi di concorrenza” non è solo un termine da colloquio. Nel magazzino condiviso chiamato heap (Heap), innumerevoli lavoratori-thread entrano ed escono senza sosta. Mettere ordine in questo caos è ciò che dimostra la bravura di uno sviluppatore backend.

Ma aspetta: quando i lavoratori aumentano, come si decide chi deve fare cosa per primo? Come fa il direttore della fabbrica (OS) a gestire centinaia di lavoratori? La prossima volta parleremo del compito più spinoso del direttore: “scheduling e cambio di contesto (Context Switching)”.

Lascia un commento