Utilisez la primitive de synchronisation std::mutex en C++
Cet article montrera comment utiliser la primitive de synchronisation std::mutex
en C++.
Utilisez std::mutex
pour protéger l’accès aux données partagées entre les threads en C++
Généralement, les primitives de synchronisation sont des outils permettant au programmeur de contrôler en toute sécurité l’accès aux données partagées dans les programmes qui utilisent la concurrence.
Étant donné que la modification non ordonnée des emplacements de mémoire partagée à partir de plusieurs threads produit des résultats erronés et un comportement imprévisible du programme, il appartient au programmeur de garantir que le programme s’exécute de manière déterministe. La synchronisation et d’autres sujets en programmation concurrente sont assez complexes et nécessitent souvent des connaissances approfondies sur plusieurs couches de caractéristiques logicielles et matérielles dans les systèmes informatiques modernes.
Ainsi, nous supposerons une certaine connaissance préalable de ces concepts tout en couvrant une très petite partie du sujet de synchronisation dans cet article. A savoir, nous allons introduire le concept d’exclusion mutuelle, également connu sous le nom de mutex
(souvent, le même nom est donné aux noms d’objets dans les langages de programmation, par exemple std::mutex
).
Un mutex est un type de mécanisme de verrouillage qui peut entourer une section critique du programme et garantir que l’accès à celle-ci est protégé. Lorsque nous disons que la ressource partagée est protégée, cela signifie que si un thread effectue l’opération d’écriture sur l’objet partagé, les autres threads ne fonctionneront pas tant que l’ancien thread n’aura pas terminé l’opération.
Notez qu’un tel comportement peut ne pas être optimal pour certains problèmes, car des conflits de ressources, une pénurie de ressources ou d’autres problèmes liés aux performances peuvent survenir. Ainsi, d’autres mécanismes abordent ces questions et offrent des caractéristiques différentes au-delà de la portée de cet article.
Dans l’exemple suivant, nous présentons l’utilisation de base de la classe std::mutex
telle que fournie par la STL C++. Notez que le support standard du threading a été ajouté depuis la version C++11.
Dans un premier temps, nous devons construire un objet std::mutex
qui peut ensuite être utilisé pour contrôler l’accès à la ressource partagée. std::mutex
a deux fonctions membres de base - lock
et unlock
. L’opération lock
est généralement appelée avant la modification de la ressource partagée et unlock
est appelée après la modification.
Le code inséré entre ces appels est appelé section critique. Même si l’ordre précédent de mise en page du code est correct, C++ fournit une autre classe de modèle utile - std::lock_guard
, qui peut automatiquement déverrouiller le mutex
donné lorsque la portée est laissée. La principale raison d’utiliser lock_guard
au lieu d’utiliser directement les fonctions membres lock
et unlock
est de garantir que le mutex
sera déverrouillé dans tous les chemins de code même si les exceptions sont levées. Ainsi, notre exemple de code utilisera également cette dernière méthode pour démontrer l’utilisation de std::mutex
.
Le programme main
est construit pour créer deux conteneurs vecteurs
d’entiers aléatoires, puis pousser le contenu des deux vers une liste. La partie délicate est que nous voulons utiliser plusieurs threads pour ajouter des éléments dans la liste.
En fait, nous invoquons la fonction generateNumbers
avec deux threads, mais ceux-ci opèrent sur des objets différents, et la synchronisation est inutile. Une fois les entiers générés, la liste peut être remplie en appelant la fonction addToList
.
Notez que cette fonction commence par la construction lock_guard
et inclut ensuite les opérations qui doivent être effectuées sur la liste. Dans ce cas, il appelle uniquement la fonction push_back
sur l’objet liste donné.
#include <chrono>
#include <iostream>
#include <list>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
using std::cout;
using std::endl;
using std::list;
using std::string;
using std::vector;
std::mutex list1_mutex;
const int MAX = 1000;
const int NUMS_TO_GENERATE = 1000000;
void addToList(const int &num, list<int> &l) {
std::lock_guard<std::mutex> guard(list1_mutex);
l.push_back(num);
}
void generateNumbers(vector<int> &v) {
for (int n = 0; n < NUMS_TO_GENERATE; ++n) {
v.push_back(std::rand() % MAX);
}
}
int main() {
list<int> list1;
vector<int> vec1;
vector<int> vec2;
std::thread t1(generateNumbers, std::ref(vec1));
std::thread t2(generateNumbers, std::ref(vec2));
t1.join();
t2.join();
cout << vec1.size() << ", " << vec2.size() << endl;
for (int i = 0; i < NUMS_TO_GENERATE; ++i) {
std::thread t3(addToList, vec1[i], std::ref(list1));
std::thread t4(addToList, vec2[i], std::ref(list1));
t3.join();
t4.join();
}
cout << "list size = " << list1.size() << endl;
return EXIT_SUCCESS;
}
Production:
1000000, 1000000
list size = 2000000
Dans l’extrait de code précédent, nous avons choisi de créer deux threads séparés à chaque itération de la boucle for
et de les joindre au thread principal dans le même cycle. Ce scénario est inefficace car il prend un temps d’exécution précieux pour créer et détruire des threads, mais nous ne le proposons que pour démontrer l’utilisation du mutex
.
Habituellement, s’il est nécessaire de gérer plusieurs threads pendant un certain temps arbitraire dans le programme, le concept de pool de threads est utilisé. La forme la plus simple de ce concept créera un nombre fixe de threads au début de la routine de travail, puis commencera à leur attribuer des unités de travail à la manière d’une file d’attente. Lorsqu’un thread termine son unité de travail, il peut être réutilisé pour la prochaine unité de travail en attente. Attention cependant, notre code de pilote est uniquement conçu pour être une simple simulation de flux de travail multithread dans un programme.
Founder of DelftStack.com. Jinku has worked in the robotics and automotive industries for over 8 years. He sharpened his coding skills when he needed to do the automatic testing, data collection from remote servers and report creation from the endurance test. He is from an electrical/electronics engineering background but has expanded his interest to embedded electronics, embedded programming and front-/back-end programming.
LinkedIn Facebook