Omdat concurrentie moeilijk is

Gelijktijdigheid is het vermogen van een programma om meerdere taken "in uitvoering" te beheren tegelijkertijd – niet noodzakelijk parallel. De parallellisme het is de executie gelijktijdig op meerdere fysieke kernen. De verwarring tussen de twee concepten is de bron van velen bugs en slechte architectonische beslissingen.

Er zijn in 2026 vijf belangrijke concurrentiemodellen, elk met verschillende afwegingen. Niet er is absoluut het ‘beste’ model: de keuze hangt af van het type werklast (I/O-gebonden vs CPU-gebonden), taal-, ecosysteem- en latentievereisten.

Model 1: OS-threads (Java, C++)

Het meest traditionele model: elke gelijktijdigheidseenheid is een draad van het besturingssysteem. De kernel zorgt voor planning, contextwisselingen en communicatie tussen threads via mutex-beveiligd gedeeld geheugen.

// Java: thread tradizionale vs virtual thread (Java 21)
// Thread OS tradizionale — costoso: ~1MB stack, scheduling kernel
Thread platformThread = new Thread(() -> {
    processRequest(); // blocca il thread OS durante I/O
});
platformThread.start();

// Virtual Thread (Java 21, Project Loom) — leggero: ~2KB stack iniziale
Thread virtualThread = Thread.ofVirtual().start(() -> {
    processRequest(); // blocca solo il virtual thread, non il carrier
});

// Un milione di virtual thread sono praticabili
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 1_000_000; i++) {
        executor.submit(() -> handleRequest());
    }
} // attende il completamento

Pluspunten: Eenvoudig mentaal model, automatisch gebruik maken van meerdere cores, volwassen bibliotheken

Tegen: Dure OS-threads (1MB+ stack, overhead omschakeling), race-omstandigheden voor gedeeld geheugen, beperkte schaling tot duizenden threads

Wanneer: CPU-gebonden werk, Java/C++ met threadpool, gemengde belastingen

Patroon 2: Gebeurtenislus met één thread (Node.js, JavaScript)

JavaScript is single-threaded: er is slechts één uitvoeringsthread en een gebeurtenislus beheert terugbelverzoeken. Asynchrone I/O (netwerk, bestandssysteem) wordt gedelegeerd aan het besturingssysteem via libuv en voltooide bewerkingen worden in de terugbelwachtrij geplaatst.

// Node.js: event loop in azione
// Tutto esegue sullo stesso thread — nessun race condition!

const http = require('http');

http.createServer((req, res) => {
    // Questa callback non blocca il thread
    fetchUserData(req.userId)
        .then(user => {
            return fetchOrders(user.id); // altra I/O non bloccante
        })
        .then(orders => {
            res.json({ user, orders });
        })
        .catch(err => res.status(500).json({ error: err.message }));
}).listen(3000);

// Async/await (zucchero sintattico sopra Promise):
async function handleRequest(req, res) {
    const user = await fetchUserData(req.userId);   // non blocca il thread
    const orders = await fetchOrders(user.id);       // non blocca il thread
    res.json({ user, orders });
}

Pluspunten: Geen racecondities (single-thread), zeer hoge gelijktijdigheid voor I/O-gebonden, enorm npm-ecosysteem

Tegen: CPU-gebonden blokkeert alles, callback hell (verzacht door async/await), Worker Threads voor echt parallellisme

Wanneer: API-server met veel gelijktijdige I/O-verzoeken, realtime apps, BFF-laag

Model 3: Goroutine en Kanaal (Go)

Ga werktuigen Communiceren van sequentiële processen (CSP): ultralichte goroutines (Initiële stapel van 2 KB, groeit dynamisch) gecommuniceerd via getypte kanalen. De Go-mantra: "Communiceer niet door herinneringen te delen; deel herinneringen door te communiceren."

// Go: goroutine e channel
package main

import (
    "fmt"
    "sync"
)

// Fan-out/Fan-in pattern con goroutine
func processItems(items []Item) []Result {
    results := make(chan Result, len(items))
    var wg sync.WaitGroup

    for _, item := range items {
        wg.Add(1)
        go func(i Item) {           // avvia goroutine — ~2KB stack
            defer wg.Done()
            result := processItem(i)  // eseguito concorrentemente
            results <- result
        }(item)
    }

    // Chiudi il channel quando tutte le goroutine completano
    go func() {
        wg.Wait()
        close(results)
    }()

    // Raccoglie i risultati
    var collected []Result
    for r := range results {
        collected = append(collected, r)
    }
    return collected
}

// Channel per comunicazione sicura tra goroutine
func producer(ch chan<- int) {
    for i := 0; i < 10; i++ {
        ch <- i    // invia sul channel (blocca se pieno)
    }
    close(ch)
}

func consumer(ch <-chan int) {
    for v := range ch {     // riceve finché il channel è aperto
        fmt.Println(v)
    }
}

Pluspunten: Ultralichte goroutines (miljoenen haalbaar), kanalen voorkomen raceomstandigheden, runtime beheert de planning

Tegen: CSP-model vereist leren, goroutinelek als kanaal niet gesloten is, geen generieke geneesmiddelen vóór Go 1.18

Wanneer: Backend-services met hoge I/O-concurrency, datapijplijnen, CLI-tools

Patroon 4: Async/Await (Python, Rust)

Het async/wachten is coöperatieve concurrentie: taken geven expliciet de controle op I/O-wachtpunten (await). In tegenstelling tot de JavaScript-gebeurtenislus (ingebouwde runtime), Python en Rust vereisen een expliciete runtime (asyncio, Tokyo).

// Python: asyncio con TaskGroup (Python 3.11+)
import asyncio
import aiohttp

async def fetch_url(session: aiohttp.ClientSession, url: str) -> str:
    async with session.get(url) as response:
        return await response.text()

async def fetch_all_parallel(urls: list[str]) -> list[str]:
    async with aiohttp.ClientSession() as session:
        # TaskGroup garantisce che tutti i task completino o vengano cancellati
        async with asyncio.TaskGroup() as tg:
            tasks = [tg.create_task(fetch_url(session, url)) for url in urls]

    return [task.result() for task in tasks]

# Rust: Tokio async/await (zero-cost)
use tokio::time::{sleep, Duration};

async fn fetch_data(id: u64) -> String {
    sleep(Duration::from_millis(100)).await;  // simula I/O
    format!("data_{}", id)
}

#[tokio::main]
async fn main() {
    // Join concorrente senza allocazioni aggiuntive (zero-cost)
    let (r1, r2, r3) = tokio::join!(
        fetch_data(1),
        fetch_data(2),
        fetch_data(3),
    );
    println!("{}, {}, {}", r1, r2, r3);
}

Pluspunten: Expliciete controle over wachtpunten, geen racevoorwaarden op de gedeelde stapel, nulkosten in Rust

Tegen: "Asynchrone ziekte" (elke functie moet async zijn als deze async aanroept), complexere foutopsporing

Wanneer: Python voor I/O-gebonden (webscraping, API-aanroepen), Rust voor krachtige systemen

Model 5: Acteursmodel (Erlang/Elixir, Akka)

Het actormodel is het meest geïsoleerd: elke actor heeft zijn eigen privéstaat en communiceert alleen via berichten. Er is geen gedeeld geheugen, er zijn geen mutexen – dat is iedere acteur een onafhankelijk lichtgewichtproces.

// Elixir: GenServer (actor model)
defmodule Counter do
  use GenServer

  # Interfaccia pubblica
  def start_link(initial \\ 0) do
    GenServer.start_link(__MODULE__, initial, name: __MODULE__)
  end

  def increment() do
    GenServer.call(__MODULE__, :increment)
  end

  def get_count() do
    GenServer.call(__MODULE__, :get)
  end

  # Implementazione (private)
  def init(initial), do: {:ok, initial}

  def handle_call(:increment, _from, count) do
    {:reply, count + 1, count + 1}   # reply, valore_risposta, nuovo_stato
  end

  def handle_call(:get, _from, count) do
    {:reply, count, count}
  end
end

# La BEAM VM può avere milioni di processi leggeri
# con supervisione automatica (OTP supervisor tree)

Pluspunten: Totale isolatie (acteurcrash verspreidt zich niet), fouttolerantie door ontwerp, native gedistribueerd

Tegen: Overhead van berichtserialisatie en het debuggen van systemen met veel actoren is complex

Wanneer: Fouttolerante gedistribueerde systemen, telecom, realtime gaming, IoT met miljoenen verbindingen

Vergelijkende benchmark

Competitie: Gids voor modelselectie

  • Veel I/O-verzoeken (web-API-server): Ga voor goroutine of Node.js-gebeurtenislus – beide schalen naar tienduizenden gelijktijdigheden
  • CPU-gebonden (ML-inferentie, codering): Java- of Go OS-threads met werkrollenpools: gebruik alle cores
  • Ultra-lage latentie (handelen, gamen): Rust Tokyo of Go - geen overhead tijdens de runtime
  • Gedistribueerde fouttolerantie (telco, IoT): Elixir/Erlang Acteursmodel — native supervisieboom
  • Datawetenschap, scripting: Python-asyncio voor I/O, multiprocessing voor CPU-gebonden
  • Java Enterprise-team: Java 21 Virtual Threads - hetzelfde mentale model als klassieke threads, schalen zoals goroutine

Conclusies

Er bestaat geen universeel superieur concurrentiemodel. De juiste keuze hangt ervan af werklast (I/O versus CPU), door het team (vaardigheden en voorkeuren), door het ecosysteem (beschikbare bibliotheken) en niet-functionele vereisten (latentie, doorvoer, fouttolerantie).

De volgende artikelen in de serie gaan gedetailleerd in op elk model: Laten we beginnen de JavaScript-gebeurtenislus, het meest onbegrepen onderdeel van Node.js.

Volgende in de serie Gebeurtenislus JavaScript →