std::thread, std::jthread e gestione dei thread
Impara a creare e gestire i thread con std::thread e con std::jthread del C++20. Comprendi la differenza tra join e detach, la cancellazione cooperativa con gli stop token, la memorizzazione thread-local e la gestione delle eccezioni attraverso i confini tra thread.
Perché la concorrenza?
Le CPU moderne hanno più core e, per sfruttarli, il tuo programma deve eseguire il lavoro in modo concorrente. Il C++11 ha introdotto una libreria di threading portabile in , e il C++20 l'ha migliorata con std::jthread. Un thread è un percorso di esecuzione indipendente che condivide lo stesso spazio di indirizzamento del thread che l'ha creato — ed è proprio qui che stanno la sua forza e il suo pericolo.
A differenza dei processi, i thread condividono la memoria. Questo rende veloce la comunicazione, ma introduce anche i data race se due thread leggono e scrivono la stessa variabile senza sincronizzazione. Prima di affrontare la sincronizzazione (nella prossima lezione), impariamo a creare, gestire e arrestare in modo pulito i thread.
Creare thread con std::thread
Crei un std::thread passandogli un callable (funzione, lambda, functor) e gli eventuali argomenti. Il thread inizia a eseguire immediatamente alla costruzione.
#include <iostream>
#include <thread>
void greet(const std::string& name, int times) {
for (int i = 0; i < times; ++i) {
std::cout << "Hello, " << name << "! (" << i + 1 << ")\n";
}
}
int main() {
// Launch a thread running greet("Alice", 3)
std::thread t1(greet, "Alice", 3);
// Lambda-based thread
std::thread t2([]() {
std::cout << "Running in a lambda thread\n";
});
// MUST join or detach before t1/t2 are destroyed
t1.join(); // block until t1 finishes
t2.join();
std::cout << "All threads finished\n";
return 0;
}join() vs detach()
Ogni std::thread che rappresenta un thread in esecuzione deve essere sottoposto a join oppure a detach prima che venga eseguito il suo distruttore. Se te ne dimentichi, viene chiamata std::terminate() e il tuo programma va in crash.
join() blocca il thread chiamante finché il thread di destinazione non termina. È l'impostazione predefinita sicura — sai che il thread ha finito e che i suoi risultati sono pronti.
detach() recide il legame tra l'oggetto std::thread e il thread del sistema operativo. Il thread continua a girare in background (un "thread daemon") e non puoi più fargli join. Fai attenzione: se il thread staccato accede a variabili sullo stack della funzione che l'ha generato e quella funzione ritorna, ottieni comportamento indefinito.
Non chiamare mai detach() su un thread che cattura o referenzia variabili locali dello scope che l'ha creato. Quando la funzione creatrice ritorna, quelle variabili locali vengono distrutte e il thread staccato accede a memoria pendente. Questo è comportamento indefinito e spesso si manifesta come crash sporadici. Se devi proprio fare detach, passa i dati per valore oppure usa std::shared_ptr per estenderne il tempo di vita.
std::jthread — il thread RAII (C++20)
std::jthread risolve due problemi di std::thread: (1) esegue automaticamente il join nel suo distruttore, quindi non puoi dimenticartene, e (2) supporta la cancellazione cooperativa tramite std::stop_token.
#include <iostream>
#include <thread>
#include <chrono>
void worker(std::stop_token stoken, int id) {
while (!stoken.stop_requested()) {
std::cout << "Worker " << id << " running...\n";
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
std::cout << "Worker " << id << " stopping gracefully\n";
}
int main() {
std::jthread jt1(worker, 1); // stop_token is auto-passed as first arg
std::jthread jt2(worker, 2);
std::this_thread::sleep_for(std::chrono::seconds(1));
// Request both threads to stop
jt1.request_stop();
jt2.request_stop();
// No need to call join() — jthread's destructor does it
std::cout << "Main thread exiting\n";
return 0;
}Interrogare l'hardware e memorizzazione thread-local
std::thread::hardware_concurrency() restituisce il numero di thread concorrenti supportati dall'hardware. La durata di memorizzazione thread_local dà a ogni thread la propria copia di una variabile.
#include <iostream>
#include <thread>
#include <vector>
thread_local int tls_counter = 0; // each thread gets its own copy
void count_work(int id) {
for (int i = 0; i < 1000; ++i) {
++tls_counter; // no data race — each thread has its own
}
std::cout << "Thread " << id << ": tls_counter = " << tls_counter << "\n";
}
int main() {
unsigned int n = std::thread::hardware_concurrency();
std::cout << "Hardware concurrency: " << n << " threads\n";
std::vector<std::thread> threads;
for (unsigned int i = 0; i < n; ++i) {
threads.emplace_back(count_work, i);
}
for (auto& t : threads) {
t.join();
}
// Each thread printed 1000 — they don't share tls_counter
return 0;
}In C++20 e versioni successive, preferisci std::jthread a std::thread. Esegue automaticamente il join nel suo distruttore, eliminando il rischio che venga chiamata std::terminate se ti dimentichi di fare join. Il suo meccanismo integrato std::stop_token offre un modo pulito e standardizzato per richiedere la cancellazione cooperativa — non serve costruirti a mano un flag atomic.
Gestione delle eccezioni attraverso i thread
Un'eccezione lanciata dentro un thread e non catturata all'interno di quel thread provoca la chiamata a std::terminate(). Le eccezioni non si propagano automaticamente al thread che esegue il join.
Per trasferire le eccezioni tra thread, catturale con std::current_exception() all'interno del thread worker e conservale in uno std::exception_ptr. Il thread che fa join può poi rilanciarle con std::rethrow_exception(). Il meccanismo di std::future (trattato nella Lezione 4) fa tutto questo automaticamente.
std::threadinizia a eseguire immediatamente; devi chiamarejoin()odetach()prima che venga eseguito il suo distruttorestd::jthread(C++20) esegue il join automaticamente nel distruttore e supporta la cancellazione cooperativa tramitestd::stop_tokenthread_localdà a ogni thread la propria copia indipendente di una variabile — senza bisogno di sincronizzazionestd::thread::hardware_concurrency()ti dice quanti thread l'hardware può eseguire in parallelo- Le eccezioni in un thread non si propagano al chiamante — usa
std::exception_ptrostd::futureper trasferirle - Non lasciare mai che un thread staccato referenzi variabili locali dello scope che l'ha generato
Quiz — Metti alla prova le tue conoscenze
(15 XP)1. Cosa succede se un oggetto `std::thread` che rappresenta un thread joinable viene distrutto senza chiamare `join()` o `detach()`?
2. In che modo `std::jthread` supporta la cancellazione cooperativa?
3. Cosa fa la parola chiave `thread_local`?