Ponieważ konkurencja jest trudna

Współbieżność to zdolność programu do zarządzania wieloma zadaniami „w toku” w tym samym czasie – niekoniecznie równolegle. The równoległość to jest wykonanie jednocześnie na wielu rdzeniach fizycznych. Źródłem wielu jest zamieszanie między tymi dwoma pojęciami błędów i złych decyzji architektonicznych.

W roku 2026 będzie dostępnych pięć głównych modeli konkurencji, a każdy z nich będzie wymagał innych kompromisów. Nie istnieje absolutnie „najlepszy” model: wybór zależy od rodzaju obciążenia (związane z we/wy vs związane z procesorem), wymagania dotyczące języka, ekosystemu i opóźnień.

Model 1: wątki systemu operacyjnego (Java, C++)

Najbardziej tradycyjny model: każda jednostka współbieżności to: wątek systemu operacyjnego. Jądro obsługuje planowanie, przełączanie kontekstu i komunikację między wątkami za pośrednictwem pamięci współdzielonej chronionej muteksem.

// 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

Plusy: Prosty model mentalny, automatyczne wykorzystanie wielu rdzeni, dojrzałe biblioteki

Przeciwko: Drogie wątki systemu operacyjnego (stos 1 MB+, narzut przełączania), warunki wyścigu w pamięci współdzielonej, ograniczone skalowanie do tysięcy wątków

Gdy: Praca związana z procesorem, Java/C++ z pulą wątków, obciążenia mieszane

Wzorzec 2: Pętla zdarzeń jednowątkowych (Node.js, JavaScript)

JavaScript jest jednowątkowy: istnieje tylko jeden wątek wykonania i pętla zdarzeń zarządza wywołaniami zwrotnymi. Asynchroniczne operacje we/wy (sieć, system plików) są delegowane do systemu operacyjnego przez libuv a zakończone operacje umieszczane są w kolejce wywołań zwrotnych.

// 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 });
}

Plusy: Brak warunków wyścigu (pojedynczy wątek), bardzo wysoka współbieżność dla powiązanego z we/wy, ogromnego ekosystemu npm

Przeciwko: Związane z procesorem blokuje wszystko, piekło wywołań zwrotnych (ograniczone przez async/await), wątki robocze dla prawdziwej równoległości

Gdy: Serwer API z wieloma współbieżnymi żądaniami I/O, aplikacjami czasu rzeczywistego, warstwą BFF

Model 3: Goroutine i kanał (Go)

Idź do narzędzi Komunikowanie procesów sekwencyjnych (CSP): ultralekkie gorutyny (początkowy stos 2KB, rośnie dynamicznie) przekazywany za pośrednictwem kanałów typowanych. Mantra Go: „Nie komunikuj się, dzieląc się pamięcią; udostępniaj pamięć, komunikując się”.

// 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)
    }
}

Plusy: Ultralekkie goroutines (miliony wykonalnych), kanały zapobiegają warunkom wyścigowym, środowisko wykonawcze zarządza harmonogramem

Przeciwko: Model CSP wymaga nauki, wyciek goroutine, jeśli kanał nie jest zamknięty, żadnych typów generycznych przed wersją 1.18

Gdy: Usługi backendowe z dużą współbieżnością we/wy, potoki danych, narzędzia CLI

Wzorzec 4: Asynchronizacja/Oczekiwanie (Python, Rust)

Async/await jest rywalizacja kooperacyjna: zadania wyraźnie rezygnują z kontrola w punktach oczekiwania we/wy (await). W przeciwieństwie do pętli zdarzeń JavaScript (wbudowane środowisko wykonawcze), Python i Rust wymagają jawnego środowiska wykonawczego (asyncio, Tokio).

// 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);
}

Plusy: Wyraźna kontrola nad punktami oczekiwania, brak warunków wyścigu na współdzielonym stosie, zerowy koszt w Rust

Przeciwko: „Choroba asynchroniczna” (każda funkcja musi być asynchroniczna, jeśli wywołuje asynchronizację), bardziej złożone debugowanie

Gdy: Python do zastosowań związanych z we/wy (skrobanie sieci, wywołania API), Rust do systemów o wysokiej wydajności

Model 5: Model aktora (Erlang/Elixir, Akka)

Model aktora jest najbardziej izolowany: każdy aktor ma swój własny stan prywatny i komunikuje się tylko poprzez wiadomości. Nie ma pamięci współdzielonej, nie ma muteksów – jest nimi każdy aktor niezależny, lekki proces.

// 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)

Plusy: Całkowita izolacja (awaria aktora nie rozprzestrzenia się), zaprojektowana odporność na błędy, dystrybucja natywna

Przeciwko: Narzut serializacji wiadomości, debugowanie systemów z wieloma aktorami jest złożone

Gdy: Odporne na awarie systemy rozproszone, telekomunikacja, gry w czasie rzeczywistym, IoT z milionami połączeń

Benchmark porównawczy

Konkurs: Przewodnik po wyborze modelu

  • Wiele żądań we/wy (serwer Web API): Przejdź do pętli zdarzeń Node.js lub goroutine — oba skalują się do dziesiątek tysięcy jednoczesnych operacji
  • Związane z procesorem (wnioskowanie ML, kodowanie): Wątki Java lub Go OS z pulami procesów roboczych — użyj wszystkich rdzeni
  • Bardzo niskie opóźnienia (handel, gry): Rust Tokyo lub Go — zerowe obciążenie w czasie wykonywania
  • Rozproszona odporność na błędy (telco, IoT): Model aktora Elixir/Erlang — natywne drzewo nadzoru
  • Analityka danych, skrypty: Asyncio Pythona dla wejścia/wyjścia, przetwarzanie wieloprocesorowe dla procesora
  • Zespół korporacyjny Java: Wirtualne wątki Java 21 — ten sam model mentalny co klasyczne wątki, skaluje się jak goroutine

Wnioski

Nie ma uniwersalnego, lepszego modelu konkurencji. Właściwy wybór zależy od workload (I/O vs CPU), by the team (skills and preferences), by the ecosystem (available libraries) i wymagania niefunkcjonalne (opóźnienie, przepustowość, odporność na błędy).

Kolejne artykuły z serii szczegółowo omawiają każdy model: Zacznijmy od pętla zdarzeń JavaScript, najbardziej źle rozumiany komponent Node.js.

Następny w serii JavaScript pętli zdarzeń →