Processen en threads: alleen werken of de kloontechniek gebruiken

Kennis als een uit het hoofd geleerd sollicitatiepraatje van één minuut

“Wat is het verschil tussen een proces en een thread?”

Toen ik nog naar mijn eerste baan zocht, stond deze vraag altijd in de ‘top 10 van vaste sollicitatievragen’. Ik antwoordde als een machine.

“Een proces is een programma dat wordt uitgevoerd, en een thread is een uitvoeringsstroom binnen een proces. Processen delen geen middelen, threads wel.”

De interviewer knikte, en ik dacht dat ik het concept volledig begreep. Maar tot ik in de praktijk tegen ‘gelijktijdigheidsproblemen’ (Concurrency Issue) aanliep, had ik geen flauw idee van het echte gevaar dat in die zin verscholen zat.

“Waarom zou het aantal views met 100 moeten stijgen, maar stijgt het maar met 98?” “Waarom loopt de website vast zodra iemand een Excel-download start?”

Mijn code werkte perfect zolang ik hem alleen draaide, maar zodra er meerdere gebruikers tegelijk binnenkwamen, werd het een complete chaos. Ik dacht dat ik het gewoon kon oplossen door meer ‘arbeiders’ in te zetten. Ik wist niet dat de kosten om ze te managen (Context Switching) exponentieel stijgen naarmate je er meer hebt.

Alleen werken is vredig, maar met z’n allen breekt de oorlog uit.

Fabrieken en arbeiders in het digitale distributiecentrum

Laten we teruggaan naar het universum van ons ‘digitale distributiecentrum’. Een programma uitvoeren op een computer is hetzelfde als een ‘fabriek’ (Factory) openen binnen het distributiecentrum.

1. Proces (Process): een zelfstandige fabriek

  • Definitie: een ‘zelfstandige werkeenheid’ die middelen toegewezen krijgt van het besturingssysteem (de fabrieksdirecteur).
  • Kenmerk: elke fabriek heeft een eigen gebouw. Als de fabriek ernaast instort (een fout), blijft mijn fabriek veilig.
  • Kosten: voor een fabriek heb je grond (geheugen) nodig en moet je ook het bedrijf laten registreren (PID). ‘Duur en zwaar.’
  • Delen: in principe kun je geen spullen uit de fabriek ernaast pakken. (Daarvoor heb je een ingewikkelde vorm van communicatie nodig: IPC.)

2. Thread: de arbeiders in de fabriek

  • Definitie: de ‘arbeider’ die binnen het proces (de fabriek) het eigenlijke werk doet.
  • Kenmerk: één fabriek kan meerdere arbeiders hebben (multithreading).
  • Kosten: een nieuwe arbeider aannemen is veel ‘goedkoper en lichter’ dan een nieuwe fabriek bouwen.
  • Delen: dit is de kern. De arbeiders gebruiken samen de ‘kantine (Heap) en de pauzeruimte (Data)’.
Een thread heeft tegelijk een ‘gedeelde ruimte’ (Heap) en een ‘eigen ruimte’ (Stack).

[Code Verification] De tragedie van het delen (gelijktijdigheidsproblemen)

“Threads delen middelen.” In het sollicitatiegesprek klonk dat als een voordeel, maar in de praktijk kan het de kiem van een ‘ramp’ zijn.

Weet je de heap (Heap) nog waar we het de vorige keer over hadden? Threads delen dit heapgebied. Met andere woorden: thread B kan langskomen en de gegevens overschrijven waar thread A net mee bezig is.

Dit noemen we een ‘race condition’ (Race Condition). Laten we het met code controleren.

public class RaceConditionTest {
    static int count = 0; // gedeelde variabele, opgeslagen in de heap (Heap)

    public static void main(String[] args) throws InterruptedException {
        // twee arbeiders (threads) aannemen
        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(); // aan het werk!
        t2.start(); // aan het werk!
        
        t1.join(); // wachten tot de dienst erop zit
        t2.join(); // wachten tot de dienst erop zit
        
        System.out.println("Eindresultaat: " + count);
    }
}

Verwacht resultaat: twee arbeiders tellen elk 10.000 keer op, dus er zou 20,000 uit moeten komen.

Werkelijk resultaat: 15,482, 18,931… Elke keer dat je het uitvoert, krijg je een andere waarde, en nooit 20.000.

De reden:

  1. Thread A leest count(100) uit de heap en haalt het naar de stack.
  2. Ondertussen leest thread B ook count(100) uit de heap.
  3. A maakt er 101 van en slaat het op in de heap.
  4. Ook B maakt er (omdat hij 100 had gelezen) 101 van en slaat het op in de heap.
  5. Er is twee keer opgeteld, maar het resultaat is alsof er maar één keer is opgeteld.

Dit is de ware aard van de bug waarbij in de praktijk “soms gegevens zomaar verdwijnen”. Het is een tragedie die ontstaat doordat arbeiders zonder met elkaar te praten in hetzelfde grootboek zitten te schrijven.

Als je deze situatie vergelijkt met een database (DB), is het precies hetzelfde als ‘meerdere query’s die tegelijk worden afgevuurd zonder transactie (Transaction)’. Het saldo van een rekening wordt tegelijk aangepast zonder beveiliging (Lock), en dan gebeurt er een vreselijk ongeluk waarbij geld gewoon verdampt. Zoals we in de DB gegevens beschermen met ROLLBACK of COMMIT, heb je ook op codeniveau absoluut een vergrendeling nodig, zoals Synchronized.

De trade-off in de praktijk: multiprocess vs multithread

Wanneer gebruik je in de praktijk dan wat?

1. De keuze van de Chrome-browser: multiprocess

Bij de oude Internet Explorer viel de hele browser uit als één tabblad vastliep (multithreaded aanpak). Chrome start daarentegen elk tabblad als een ‘apart proces (fabriek)’.

  • Voordeel: ook als één tabblad niet meer reageert (de fabriek ontploft), blijven de andere tabbladen (de fabrieken ernaast) gewoon heel. (Stabiliteit)
  • Nadeel: het slokt enorm veel geheugen op. Daarom is Chrome een ‘RAM-vretend nijlpaard’ geworden. (Kosten)

2. De keuze van webservers (Spring, Node.js enz.): multithread

Een server moet verzoeken van duizenden mensen afhandelen. Als je voor elk verzoek een proces (fabriek) bouwt, gaat de server plat. Daarom zet je binnen één proces een groot aantal threads (arbeiders) in om alles af te handelen.

  • Voordeel: het verbruikt weinig middelen en is snel. (Efficiëntie)
  • Nadeel: als één thread een fout veroorzaakt die het proces doodt, gaat de hele server onderuit. Bovendien moet de ontwikkelaar de gelijktijdigheidsproblemen die we net zagen zelf voorkomen (Synchronized).
Bouw je een veilig maar duur ‘fort’, of laat je snelle maar riskante ‘drones’ vliegen?

Tot slot: de kloontechniek brengt verantwoordelijkheid met zich mee

Vandaag hebben we gekeken naar de manier waarop de arbeiders werken: ‘processen en threads’.

  • Proces: een zelfstandige fabriek, veilig maar duur.
  • Thread: een collega met wie je de ruimte deelt, licht en snel, maar met kans op botsingen.

Nu weet je dat ‘gelijktijdigheidsproblemen’ niet zomaar een term voor sollicitatiegesprekken is. Door het gedeelde magazijn dat heap (Heap) heet, lopen talloze thread-arbeiders onophoudelijk in en uit. Orde scheppen in die chaos, daar blijkt het vakmanschap van een backendontwikkelaar uit.

Maar wacht even: als er steeds meer arbeiders komen, hoe bepaal je dan wie wat als eerste moet doen? Hoe houdt de fabrieksdirecteur (OS) honderden arbeiders in de hand? De volgende keer hebben we het over de lastigste taak van de fabrieksdirecteur: ‘scheduling en contextwisseling (Context Switching)’.

Plaats een reactie