C++ Multithreading
Les threads sont des unités d'exécution légères dans un programme, permettant au programme d'exécuter plusieurs tâches simultanément.
Le multithreading est une forme particulière de multitâche, qui permet à un ordinateur d'exécuter deux programmes ou plus simultanément.
En général, deux types de multitâche :basé sur les processus et basé sur les threads。
- Le multitâche basé sur les processus est l'exécution concurrente de programmes.
- Le multitâche basé sur les threads est l'exécution concurrente de fragments d'un même programme.

La programmation multithread en C++ implique de créer et de gérer plusieurs threads s'exécutant simultanément dans un programme.
C++ offre une prise en charge puissante du multithreading, en particulier dans la norme C++11 et les suivantes, grâce à<thread>la bibliothèque standard, ce qui rend la programmation multithread plus simple et plus sûre.
Description du concept
Fil
- Un thread est un flux de contrôle séquentiel unique dans l'exécution d'un programme, et plusieurs threads peuvent s'exécuter indépendamment au sein du même processus.
- Les threads partagent des ressources telles que l'espace d'adressage du processus, les descripteurs de fichiers, le tas et les variables globales, mais chaque thread possède sa propre pile, ses propres registres et son propre compteur de programme.
Concurrence et parallélisme
- Concurrence: Plusieurs tâches s'exécutent alternativement dans des tranches de temps, créant l'effet d'une exécution simultanée.
- Parallélisme: Plusieurs tâches s'exécutent simultanément sur plusieurs processeurs ou cœurs de processeur.
Les normes C++11 et ultérieures fournissent une prise en charge du multithreading, avec les composants principaux suivants :
std::thread: Utilisé pour créer et gérer des fils d'exécution.std::mutex: Utilisé pour l'exclusion mutuelle entre les fils d'exécution, empêchant plusieurs fils d'exécution d'accéder simultanément à des ressources partagées.std::lock_guardetstd::unique_lock: Utilisé pour gérer l'acquisition et la libération des verrous.std::condition_variable: Utilisé pour les variables de condition entre les fils d'exécution, coordonnant l'attente et la notification entre les fils d'exécution.std::futureetstd::promise: Utilisé pour implémenter le passage de valeurs et la synchronisation des tâches entre les fils d'exécution.
Création de threads
Après C++11, une nouvelle bibliothèque standard de fils d'exécution a été ajoutée.std::thread,std::thread dans <thread>Déclaré dans le fichier d'en-tête, donc en utilisantstd::threadil faut incluredans <thread>fichier d'en-tête.
Remarque :Auparavant, certains compilateurs utilisaient l'option de compilation pour C++11 comme-std=c++11:
g++ -std=c++11 test.cpp
std::thread
Créer un fil d'exécution au moyen d'un pointeur de fonction est la méthode la plus élémentaire : Utilisationg++ -std=c++11Après compilation, la sortie d'exécution est : par l'intermédiaire de la classe Utilisationg++ -std=c++11Après compilation, la sortie d'exécution est : Les expressions Lambda peuvent définir directement du code inline pour l'exécution d'un thread : Utilisationg++ -std=c++11Après compilation, la sortie d'exécution est : join() est utilisé pour attendre qu’un thread termine son exécution. Si un objet thread est détruit directement sans appeler join() ou detach(), cela provoquera un plantage du programme. detach() sépare le thread du thread principal ; le thread s’exécute indépendamment en arrière-plan, et le thread principal ne l’attend plus. Les paramètres peuvent être passés aux threads par valeur : Si vous devez passer des paramètres par référence, vous devez utiliser std::ref : Voici un exemple complet montrant comment utiliser les trois méthodes ci-dessus pour créer des threads et effectuer la gestion des threads. Option de compilation pour utiliser C++11-std=c++11: La sortie du code ci-dessus peut différer selon les plateformes ou d'une exécution à l'autre, car l'ordre d'exécution des threads est déterminé par l'algorithme d'ordonnancement du système d'exploitation. Plusieurs threads s'exécutent simultanément, et la sortie peut être entrelacée, par exemple : En programmation multithread, la synchronisation des threads et l'exclusion mutuelle sont deux concepts très importants. Ils sont utilisés pour contrôler l'accès aux ressources partagées par plusieurs threads afin d'éviter les courses de données, les interblocages et d'autres problèmes. Un mutex est une primitive de synchronisation utilisée pour empêcher plusieurs threads d'accéder simultanément à des ressources partagées. Lorsqu'un thread doit accéder à une ressource partagée, il doit d'abord verrouiller le mutex. Si le mutex a déjà été verrouillé par un autre thread, le thread demandant le verrou sera bloqué jusqu'à ce que le mutex soit déverrouillé. std::mutex: Utilisé pour protéger les ressources partagées et éviter les courses de données.
std::lock_guardetstd::unique_lock: Gèrent automatiquement l'acquisition et la libération des verrous. Exemple d'utilisation d'un mutex : C++ propose plusieurs types de verrous pour simplifier l'utilisation et la gestion des mutex. Les types de verrous courants incluent : Exemple d'utilisation de verrous : Les variables de condition sont utilisées pour la coordination entre les threads, permettant à un ou plusieurs threads d'attendre qu'une certaine condition se produise. Elles sont généralement utilisées avec des mutex pour réaliser la synchronisation entre les threads. std::condition_variableUtilisé pour implémenter des mécanismes d'attente et de notification entre les threads. Exemple d'utilisation des variables de condition : Les opérations atomiques garantissent que l'accès aux données partagées est indivisible, ce qui signifie que dans un environnement multithread, une opération atomique s'exécute soit complètement, soit pas du tout, sans état intermédiaire. Exemple d'utilisation d'opérations atomiques : Le stockage local au thread permet à chaque thread d'avoir sa propre copie de données. Cela peut être réalisé avec le mot-clé thread_local, évitant la contention pour les ressources partagées. Exemple d'utilisation du stockage local au thread : L'interblocage se produit lorsque plusieurs threads s'attendent mutuellement pour libérer des ressources, mais qu'aucun thread ne peut continuer à s'exécuter. Les stratégies pour éviter l'interblocage incluent : std::future et std::promise : implémentation du passage de valeurs entre threads. Les files d'attente de messages (basées sur std::queue et std::mutex) mettent en œuvre une communication simple entre threads. C++17 a introduit la bibliothèque d'algorithmes parallèles ( Voir plus d'exemples : Multithreading en C++ :http://www.example.com/w3cnote/cpp-multithread-demo.html C++ std::thread: https://www.example.com/w3cnote/cpp-std-thread.html#include<thread>
std::thread thread_object(callable, args...);
callable: objets appelables, qui peuvent être des pointeurs de fonction, des objets fonction, des expressions lambda, etc.args...: transmis àcallableLa liste des paramètres de.Utilisation des pointeurs de fonction
Exemple
#include <thread>
void printMessage(int count) {
for (int i = 0; i < count; ++i) {
std::cout << "Hello from thread (function pointer)!\n";
}
}
int main() {
std::thread t1(printMessage, 5); // Créer un fil d'exécution, en passant un pointeur de fonction et des paramètres
t1.join(); // Attendre que le fil d'exécution soit terminé
return 0;
}
Hello from thread (function pointer)!
Hello from thread (function pointer)!
Hello from thread (function pointer)!
Hello from thread (function pointer)!
Hello from thread (function pointer)!
Utilisation des objets fonction
operator()Définir une méthode d'objet fonction pour créer un thread :Exemple
#include <thread>
class PrintTask {
public:
void operator()(int count) const {
for (int i = 0; i < count; ++i) {
std::cout << "Bonjour depuis le thread (objet fonction) !\n";
}
}
};
int main() {
std::thread t2(PrintTask(), 5); // Créer un thread, en passant un objet fonction et des paramètres
t2.join(); // Attendre que le fil d'exécution soit terminé
return 0;
}
Hello from thread (function object)!
Hello from thread (function object)!
Hello from thread (function object)!
Hello from thread (function object)!
Hello from thread (function object)!
Utilisation des expressions Lambda
Exemple
#include <thread>
int main() {
std::thread t3([](int count) {
for (int i = 0; i < count; ++i) {
std::cout << "Bonjour depuis le thread (lambda) !\n";
}
}, 5); // Créer un thread, en passant une expression Lambda et des paramètres
t3.join(); // Attendre que le fil d'exécution soit terminé
return 0;
}
Hello from thread (lambda)!
Hello from thread (lambda)!
Hello from thread (lambda)!
Hello from thread (lambda)!
Hello from thread (lambda)!
Gestion des threads
join()
t.join();
detach()
t.detach();
Passage de paramètres aux threads
Passage par valeur
std::thread t(func, arg1, arg2);
Passage par référence
Exemple
#include <thread>
void increment(int& x) {
++x;
}
int main() {
int num = 0;
std::thread t(increment, std::ref(num)); // Utiliser std::ref pour passer des références
t.join();
std::cout << "Valeur après incrémentation : " << num << std::endl;
return 0;
}
Exemple complet
Exemple
#include <thread>
using namespace std;
// Une fonction simple qui sert de fonction d'entrée au thread
void foo(int Z) {
for (int i = 0; i < Z; i++) {
cout << "Le thread utilise un pointeur de fonction comme paramètre appelable\n";
}
}
// Définition de la classe d'objet appelable
class ThreadObj {
public:
void operator()(int x) const {
for (int i = 0; i < x; i++) {
cout << "Le thread utilise un objet fonction comme paramètre appelable\n";
}
}
};
int main() {
cout << "Les threads 1, 2 et 3 s'exécutent indépendamment" << endl;
// Créer un thread à l'aide d'un pointeur de fonction
thread th1(foo, 3);
// Créer un thread à l'aide d'un objet fonction
thread th2(ThreadObj(), 3);
// Créer un thread à l'aide d'une expression lambda
thread th3([](int x) {
for (int i = 0; i < x; i++) {
cout << "Le thread utilise une expression lambda comme paramètre appelable\n";
}
}, 3);
// Attendre que tous les threads se terminent
th1.join(); // Attendre que le thread th1 se termine
th2.join(); // Attendre que le thread th2 se termine
th3.join(); // Attendre que le thread th3 se termine
return 0;
}
g++ -std=c++11 test.cpp
线程 1 、2 、3 独立运行
线程使用函数指针作为可调用参数
线程使用函数对象作为可调用参数
线程使用 lambda 表达式作为可调用参数
线程使用函数指针作为可调用参数
...
Synchronisation des threads et exclusion mutuelle
1. Mutex
std::mutex mtx;
mtx.lock(); // 锁定互斥锁
// 访问共享资源
mtx.unlock(); // 释放互斥锁
std::lock_guard<std::mutex> lock(mtx); // 自动锁定和解锁
// 访问共享资源
Exemple
std::mutex mtx; // Mutex global
void safeFunction() {
mtx.lock(); // Demande de verrouiller le mutex
// Accéder aux ressources partagées ou les modifier
mtx.unlock(); // Libérer le mutex
}
int main() {
std::thread t1(safeFunction);
std::thread t2(safeFunction);
t1.join();
t2.join();
return 0;
}
2. Verrous
Exemple
std::mutex mtx;
void safeFunctionWithLockGuard() {
std::lock_guard<std::mutex> lk(mtx);
// Accéder aux ressources partagées ou les modifier
}
void safeFunctionWithUniqueLock() {
std::unique_lock<std::mutex> ul(mtx);
// Accéder aux ressources partagées ou les modifier
// ul.unlock(); // Facultatif : déverrouiller manuellement
// ...
}
3. Variable de condition
std::condition_variable cv;
std::mutex mtx;
bool ready = false;
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; }); // 等待条件满足
// 条件满足后执行
Exemple
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void workerThread() {
std::unique_lock<std::mutex> lk(mtx);
cv.wait(lk, []{ return ready; }); // Attendre la condition
// Exécuter le travail lorsque la condition est remplie
}
void mainThread() {
{
std::lock_guard<std::mutex> lk(mtx);
// Préparer les données
ready = true;
} // Déverrouiller en quittant la portée
cv.notify_one(); // Notifier un thread en attente
}
4. Opérations atomiques
Exemple
#include <thread>
std::atomic<int> count(0);
void increment() {
count.fetch_add(1, std::memory_order_relaxed);
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
return count; // Devrait retourner 2
}
5. Stockage local au thread (TLS)
Exemple
#include <thread>
thread_local int threadData = 0;
void threadFunction() {
threadData = 42; // Chaque thread a sa propre copie de threadData
std::cout << "Données du thread : " << threadData << std::endl;
}
int main() {
std::thread t1(threadFunction);
std::thread t2(threadFunction);
t1.join();
t2.join();
return 0;
}
6. Interblocage et stratégies d'évitement
Communication entre threads
Exemple
std::promise<int> p;
std::future<int> f = p.get_future();
std::thread t([&p] {
p.set_value(10); // Définir la valeur, déclencher le future
});
int result = f.get(); // Obtenir la valeur
<algorithm>), où certains algorithmes prennent en charge l'exécution parallèle, en tirant parti des processeurs multicœurs pour améliorer les performances.Exemple
#include <vector>
#include <execution>
std::vector<int> vec = {1, 2, 3, 4, 5};
std::for_each(std::execution::par, vec.begin(), vec.end(), [](int &n) {
n *= 2;
});
Algorithmes parallèles dans la bibliothèque standard C++
autres extensions