C++ Multithreading : Threads, Mutex, Async et Performance

C++ Multithreading : Threads, Mutex, Async et Performance

Amine Abidi - Lead Software Engineer C++/Qt - Co-fondateur PointerLab

Publié par Amine Abidi - Lead Software Engineer C++/Qt - Co-fondateur PointerLab

Dans un monde où la performance et la réactivité sont essentielles, le multithreading en C++ s'impose comme une compétence incontournable. Grâce aux threads, vos applications peuvent tirer parti de l'exécution parallèle et de la concurrence pour exploiter pleinement les processeurs multicœurs. Le multithreading devient une compétence incontournable pour les développeurs C++. Le multithreading ne se limite pas à une simple exécution parallèle ; il constitue une stratégie fondamentale pour maximiser l'utilisation des ressources matérielles modernes. Les processeurs multicœurs sont aujourd'hui la norme, et tirer parti de cette architecture nécessite une bonne compréhension de la programmation concurrente. C++ propose une panoplie d'outils pour gérer les threads, la synchronisation, et le parallélisme, que ce soit via la bibliothèque standard ou d'autres solutions comme OpenMP ou Intel TBB. Cet article vous guide pas à pas pour comprendre et maîtriser les threads en C++.

Qu'est-ce qu'un Thread en C++ ?

Un thread (ou fil d'exécution) est une entité légère qui s'exécute indépendamment au sein d'un processus. Dans le multithreading en C++, chaque thread partage la mémoire et les ressources avec les autres threads de l'application. Cette exécution parallèle permet de gérer plusieurs tâches en même temps, réduisant ainsi les temps de traitement et améliorant la réactivité.

Pourquoi utiliser le Multithreading en C++ ?

Le multithreading permet :

  • Une exploitation maximale des processeurs multicoeurs
  • Une amélioration significative des performances
  • Une réduction des temps de réponse
  • Un traitement asynchrone fluide
  • Une meilleure répartition des tâches : interface utilisateur, calculs intensifs, communication réseau…
  • Une meilleure concurrence et gestion du parallélisme

Créer un Thread en C++ avec std::thread

#include <iostream>
#include <thread>
 
void fonction_thread() {
    std::cout << "Thread exécuté en C++ !" << std::endl;
}
 
int main() {
    std::thread mon_thread(fonction_thread);
    mon_thread.join();
    return 0;
}

Remarque : toujours utiliser join() pour synchroniser ou detach() pour exécuter en arrière-plan.

Comparer join() et detach() en C++

  • join() : le thread principal attend la fin du thread secondaire.
  • detach() : le thread s'exécute indépendamment, attention aux threads zombies !

Créer Plusieurs Threads en C++

#include <iostream>
#include <thread>
 
void tache(int id) {
    std::cout << "Thread #" << id << " en action." << std::endl;
}
 
int main() {
    std::thread t1(tache, 1);
    std::thread t2(tache, 2);
    std::thread t3(tache, 3);
 
    t1.join();
    t2.join();
    t3.join();
}

Passer des Paramètres à un Thread

#include <iostream>
#include <thread>
 
void repete_message(const std::string& message, int fois) {
    for (int i = 0; i < fois; ++i) {
        std::cout << "Thread dit : " << message << std::endl;
    }
}
 
int main() {
    std::thread mon_thread(repete_message, "Hello du thread !", 3);
    mon_thread.join();
}

Utiliser les Lambdas pour les Threads

#include <iostream>
#include <thread>
 
int main() {
    std::thread mon_thread([](){
        std::cout << "Thread lancé avec une lambda." << std::endl;
    });
    mon_thread.join();
}

Protéger les Données Partagées : std::mutex vs std::lock_guard

#include <iostream>
#include <thread>
#include <mutex>
 
std::mutex mtx;
 
void impression_sure(const std::string& msg) {
    std::lock_guard<std::mutex> verrou(mtx);
    std::cout << msg << std::endl;
}
 
int main() {
    std::thread t1(impression_sure, "Thread 1 imprime en toute sécurité.");
    std::thread t2(impression_sure, "Thread 2 imprime en toute sécurité.");
 
    t1.join();
    t2.join();
}
Caractéristique mutex lock_guard
Verrouillage manuel Oui Non (automatique)
Risque d'oubli de unlock Oui Non
Idéal pour… Code complexe Bloc simple

Threads Asynchrones : std::async

#include <iostream>
#include <future>
 
int calculer_carre(int nombre) {
    return nombre * nombre;
}
 
int main() {
    std::future<int> resultat = std::async(calculer_carre, 6);
    std::cout << "Résultat du thread : " << resultat.get() << std::endl;
}

Exemples Avancés de Multithreading en C++

Lecture et Calculs en Threads

#include <iostream>
#include <thread>
 
void lire_fichier() {
    std::cout << "Thread lit un fichier..." << std::endl;
}
 
void calculer() {
    std::cout << "Thread effectue des calculs..." << std::endl;
}
 
int main() {
    std::thread t1(lire_fichier);
    std::thread t2(calculer);
 
    t1.join();
    t2.join();
}

Interface Réactive et Traitement

Utiliser les threads pour exécuter les calculs en arrière-plan permet de garder l'interface utilisateur fluide.

Synchronisation avec std::condition_variable

La classe std::condition_variable permet aux threads de se mettre en attente jusqu'à ce qu'une condition soit remplie. Elle est souvent utilisée en combinaison avec std::mutex pour gérer l'accès aux ressources partagées de manière plus souple qu'un simple verrouillage.

Exemple :

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
 
std::mutex mtx;
std::condition_variable cv;
bool pret = false;
 
void attendre() {
    std::unique_lock<std::mutex> lck(mtx);
    cv.wait(lck, [] { return pret; });
    std::cout << "Thread réveillé !\n";
}
 
void declencher() {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    {
        std::lock_guard<std::mutex> lck(mtx);
        pret = true;
    }
    cv.notify_one();
}
 
int main() {
    std::thread t1(attendre);
    std::thread t2(declencher);
    t1.join();
    t2.join();
}

Utilisation d'un Thread Pool

Créer un grand nombre de threads peut rapidement surcharger le système. Un thread pool (ou « bassin de threads ») permet de limiter le nombre de threads actifs et de les réutiliser pour exécuter plusieurs tâches, ce qui améliore la performance et réduit la surcharge système.

Un exemple simple : vous pouvez implémenter un thread pool en utilisant une file de tâches (queue) et un vecteur de threads qui consomment ces tâches.

Erreurs Fréquentes en Multithreading

  • Ne pas utiliser join() ou detach() : peut provoquer un crash ou des threads zombies.
  • Utilisation incorrecte de mutex : peut entraîner un deadlock (blocage mutuel).
  • Accès concurrent sans protection : génère des comportements indéterminés (race conditions).
  • Création excessive de threads : surcharge CPU/mémoire et baisse de performance.
  • Mauvais usage des variables conditionnelles : peut empêcher le réveil des threads.

Thread Safety et Bonnes Pratiques

✅ Toujours utiliser join() ou detach() correctement

✅ Protéger les ressources partagées avec mutex ou lock_guard

✅ Préférer les objets immuables

✅ Surveiller l'empreinte mémoire

✅ Profiler régulièrement (Valgrind, perf, gprof…)

Optimisation de Performance Multithreading

  • Utiliser un thread pool au lieu de créer trop de threads manuellement
  • Privilégier les algorithmes parallélisables
  • Identifier les goulets d'étranglement avec des outils de mesure

FAQ sur le Multithreading en C++

Q: Un thread partage-t-il la mémoire avec les autres threads ?
R: Oui, dans un même processus, les threads partagent la mémoire.

Q: Quel outil protège les ressources partagées ?
R: std::mutex (souvent avec lock_guard).

Q: Quelle est la différence entre std::thread et std::async ?
R: std::async automatise la gestion des threads et retourne un std::future.

Q: Pourquoi utiliser plusieurs threads ?
R: Pour paralléliser le traitement et améliorer les performances.

Q: Combien de threads un programme peut-il avoir ?
R: Cela dépend du matériel et de l'OS, mais l'efficacité prime sur la quantité.

Ressources Complémentaires

Documentation officielle : cppreference.com

Tutoriels C++ : PointerLab

Conclusion

Le multithreading en C++ ouvre la porte à la programmation hautement performante et réactive. En maîtrisant les outils comme std::thread, std::mutex, std::lock_guard, std::async et std::condition_variable, les développeurs peuvent créer des systèmes robustes, réactifs, hautement performants, scalables et efficaces. À mesure que les systèmes deviennent plus complexes, l'utilisation de modèles comme les thread pools et les bibliothèques de parallélisme devient cruciale. Enfin, comprendre les pièges du multithreading est aussi important que savoir l'utiliser — une erreur minime peut avoir de graves conséquences dans un système concurrent.

Découvrez l'ensemble de notre blog sur le C++

Rejoignez la communauté C++ 🇫🇷 sur Discord !

Un espace convivial pour échanger et apprendre ensemble.