Utilisez la primitive de synchronisation std::mutex en C++

Jinku Hu 12 octobre 2023
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.

Auteur: Jinku Hu
Jinku Hu avatar Jinku Hu avatar

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