Porównanie modeli współbieżności: wątki systemu operacyjnego, pętle zdarzeń, Goroutines, aktorzy i asynchronizacja/oczekiwanie
Ostateczna mapa modeli współbieżności: wątki systemu operacyjnego (Java), zielone wątki/goroutine (Go), jednowątkowa pętla zdarzeń (Node.js), model aktora (Erlang/Elixir), async/await (Python/Rust). Kiedy którego używać, zalety/wady i punkty odniesienia.
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.







