std::thread, std::jthread et gestion des threads
Apprenez à créer et à gérer des threads avec std::thread et std::jthread de C++20. Comprenez la différence entre join et detach, l'annulation coopérative avec les stop tokens, le stockage local au thread et la gestion des exceptions entre threads.
Pourquoi la concurrence ?
Les processeurs modernes possèdent plusieurs cœurs, et pour en tirer parti, votre programme doit exécuter du travail de manière concurrente. C++11 a introduit une bibliothèque de threads portable dans , et C++20 l'a améliorée avec std::jthread. Un thread est un fil d'exécution indépendant qui partage le même espace d'adressage que le thread qui l'a créé — c'est à la fois sa force et son danger.
Contrairement aux processus, les threads partagent la mémoire. La communication est donc rapide, mais cela introduit aussi des data races (accès concurrents non synchronisés) si deux threads lisent et écrivent la même variable sans synchronisation. Avant d'aborder la synchronisation (leçon suivante), voyons d'abord comment créer, gérer et arrêter proprement des threads.
Créer des threads avec std::thread
Vous créez un std::thread en lui passant un callable (fonction, lambda, foncteur) ainsi que ses éventuels arguments. Le thread commence à s'exécuter immédiatement dès sa construction.
#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()
Tout std::thread qui représente un thread en cours d'exécution doit être soit joint (joined), soit détaché (detached) avant l'appel de son destructeur. Si vous l'oubliez, std::terminate() est appelé et votre programme plante.
join() bloque le thread appelant jusqu'à ce que le thread cible se termine. C'est le choix sûr par défaut — vous savez que le thread a fini et que ses résultats sont prêts.
detach() rompt le lien entre l'objet std::thread et le thread du système d'exploitation. Le thread s'exécute en arrière-plan (un « thread démon ») et vous ne pouvez plus le joindre. Soyez prudent : si le thread détaché accède à des variables de pile de la fonction qui l'a lancé et que cette fonction retourne, vous obtenez un comportement indéfini.
Ne faites jamais detach() sur un thread qui capture ou référence des variables locales de la portée qui l'a créé. Lorsque la fonction créatrice retourne, ces variables locales sont détruites et le thread détaché accède à de la mémoire pendante. C'est un comportement indéfini, qui se manifeste souvent par des plantages sporadiques. Si vous devez détacher, passez les données par valeur ou utilisez std::shared_ptr pour prolonger leur durée de vie.
std::jthread — le thread RAII (C++20)
std::jthread résout deux problèmes de std::thread : (1) il effectue automatiquement le join dans son destructeur, vous ne pouvez donc jamais l'oublier, et (2) il prend en charge l'annulation coopérative via 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;
}Interroger le matériel et stockage local au thread
std::thread::hardware_concurrency() renvoie le nombre de threads concurrents pris en charge par le matériel. La durée de stockage thread_local donne à chaque thread sa propre copie d'une variable.
#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;
}À partir de C++20, préférez std::jthread à std::thread. Il effectue automatiquement le join dans son destructeur, ce qui élimine le risque d'appeler std::terminate si vous oubliez de joindre le thread. Son mécanisme intégré std::stop_token offre un moyen propre et standardisé de demander une annulation coopérative — plus besoin de créer votre propre drapeau atomic.
Gestion des exceptions entre threads
Une exception levée dans un thread et non interceptée dans ce même thread provoque l'appel de std::terminate(). Les exceptions ne se propagent pas automatiquement au thread qui effectue le join.
Pour transférer des exceptions d'un thread à l'autre, capturez-les avec std::current_exception() dans le thread de travail et stockez-les dans un std::exception_ptr. Le thread qui effectue le join peut ensuite les relancer avec std::rethrow_exception(). Le mécanisme de std::future (présenté dans la leçon 4) s'en charge automatiquement.
std::threadcommence à s'exécuter immédiatement ; vous devez appelerjoin()oudetach()avant l'exécution de son destructeurstd::jthread(C++20) effectue automatiquement le join dans son destructeur et prend en charge l'annulation coopérative viastd::stop_tokenthread_localdonne à chaque thread sa propre copie indépendante d'une variable — aucune synchronisation nécessairestd::thread::hardware_concurrency()indique combien de threads le matériel peut exécuter simultanément- Les exceptions levées dans un thread ne se propagent pas à l'appelant — utilisez
std::exception_ptroustd::futurepour les transférer - Ne laissez jamais un thread détaché référencer des variables locales de la portée qui l'a lancé
Quiz — Testez vos connaissances
(15 XP)1. Que se passe-t-il si un objet `std::thread` représentant un thread joignable est détruit sans appel à `join()` ni à `detach()` ?
2. Comment `std::jthread` prend-il en charge l'annulation coopérative ?
3. Que fait le mot-clé `thread_local` ?