Modele de concurență comparate: fire de operare, bucle de evenimente, goroutine, actori și asincron/așteptare
Harta definitivă a modelelor de concurență: fire OS (Java), fire verzi/goroutine (Go), buclă de evenimente cu un singur fir (Node.js), model actor (Erlang/Elixir), asincron/așteptare (Python/Rust). Când să folosiți care, argumente pro/contra și benchmark-uri comparative.
Pentru că concurența este dificilă
Concurența este capacitatea unui program de a gestiona mai multe sarcini „în curs” în același timp — nu neapărat în paralel. The paralelism este execuția simultan pe mai multe nuclee fizice. Confuzia dintre cele două concepte este sursa multora bug-uri și decizii arhitecturale proaste.
Există cinci modele principale de competiție în 2026, fiecare cu compromisuri diferite. Nu există cel mai bun model absolut: alegerea depinde de tipul de încărcare de lucru (I/O-bound vs. Cerințele legate de CPU), limbaj, ecosistem și latență.
Model 1: fire de operare (Java, C++)
Cel mai tradițional model: fiecare unitate de concurență este a fir a sistemului de operare. Nucleul se ocupă de programare, comutări de context și comunicare între fire prin intermediul memoriei partajate protejate de mutex.
// 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
Pro: Model mental simplu, folosește automat mai multe nuclee, biblioteci mature
Împotriva: Fire de operare scumpe (stivă de 1 MB+, overhead de comutare), condiții de concurență a memoriei partajate, scalare limitată la mii de fire
Când: Lucru legat de CPU, Java/C++ cu pool de fire, încărcări mixte
Model 2: buclă de evenimente cu un singur fir (Node.js, JavaScript)
JavaScript este cu un singur thread: există doar un fir de execuție și o buclă de evenimente care
gestionează apelurile inverse. I/O asincron (rețea, sistem de fișiere) este delegat sistemului de operare
via libuv iar operațiunile finalizate sunt plasate în coada de apel invers.
// 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 });
}
Pro: Fără condiții de cursă (single-thread), concurență foarte mare pentru I/O-bound, ecosistem imens npm
Împotriva: Procesor-bound blochează totul, callback hell (atenuat de asincron/așteptare), Worker Threads pentru paralelism real
Când: Server API cu multe solicitări I/O concurente, aplicații în timp real, strat BFF
Model 3: Goroutine și Channel (Go)
Du-te unelte Comunicarea proceselor secvențiale (CSP): goroutine ultra-ușoare (stiva inițială de 2 KB, crește dinamic) comunicată prin canale tipizate. Mantra Go: „Nu comunica prin împărțirea memoriei; împărtășește memoria comunicând.”
// 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)
}
}
Pro: Goroutine ultra-ușoare (viabile de milioane), canalele previn condițiile de cursă, timpul de rulare gestionează programarea
Împotriva: Modelul CSP necesită învățare, scurgere goroutine dacă canalul nu este închis, fără generice înainte de Go 1.18
Când: Servicii de backend cu concurență mare I/O, conducte de date, instrumente CLI
Model 4: Async/Await (Python, Rust)
Async/wait este competiție cooperativă: sarcinile renunță în mod explicit la
control la punctele de așteptare I/O (await). Spre deosebire de bucla de evenimente JavaScript
(timpul de rulare încorporat), Python și Rust necesită un timp de rulare explicit (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);
}
Pro: Control explicit asupra punctelor de așteptare, fără condiții de cursă pe stiva partajată, cost zero în Rust
Împotriva: „Boala asincronă” (fiecare funcție trebuie să fie asincronă dacă apelează asincron), depanare mai complexă
Când: Python pentru I/O-bound (web scraping, apeluri API), Rust pentru sisteme de înaltă performanță
Model 5: Actor Model (Erlang/Elixir, Akka)
Modelul de actor este cel mai izolat: fiecare actor are propriul său stat privat și comunică doar prin mesaje. Nu există memorie partajată, nu există mutexuri - fiecare actor este un proces independent și ușor.
// 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)
Pro: Izolare totală (accidentul actorului nu se propagă), toleranță la erori prin proiectare, distribuită nativ
Împotriva: Serializarea mesajelor, depanarea sistemelor cu mulți actori este complexă
Când: Sisteme distribuite tolerante la erori, telecomunicații, jocuri în timp real, IoT cu milioane de conexiuni
Benchmark comparativ
Concurs: Ghid de selecție a modelelor
- Multe solicitări I/O (server web API): Goroutine sau bucla de evenimente Node.js - ambele se scalează la zeci de mii de concurente
- Legat de CPU (inferență ML, codificare): fire de operare Java sau Go OS cu grupuri de lucrători — utilizați toate nucleele
- Latență ultra-scăzută (trading, gaming): Rust Tokyo sau Go — zero overhead în timpul de rulare
- Toleranță la erori distribuită (telco, IoT): Elixir/Erlang Actor model — arbore de supraveghere nativ
- Știința datelor, scripting: Python asincron pentru I/O, multiprocesare pentru CPU-bound
- Echipa de întreprindere Java: Java 21 Virtual Threads — același model mental ca firele clasice, scale ca goroutine
Concluzii
Nu există un model de competiție universal superior. Alegerea corectă depinde de sarcina de lucru (I/O vs CPU), de echipă (abilități și preferințe), de ecosistem (biblioteci disponibile) și cerințe nefuncționale (latență, debit, toleranță la erori).
Următoarele articole din serie aprofundează în detaliu fiecare model: Să începem cu bucla de evenimente JavaScript, cea mai neînțeleasă componentă a Node.js.







