Utilice la primitiva de sincronización std::mutex en C++

Jinku Hu 12 octubre 2023
Utilice la primitiva de sincronización std::mutex en C++

Este artículo demostrará cómo utilizar la primitiva de sincronización std::mutex en C++.

Utilice std::mutex para proteger el acceso a datos compartidos entre subprocesos en C++

Generalmente, las primitivas de sincronización son herramientas para que el programador controle de forma segura el acceso a los datos compartidos en programas que utilizan la concurrencia.

Dado que la modificación desordenada de las ubicaciones de la memoria compartida de varios subprocesos produce resultados erróneos y un comportamiento impredecible del programa, depende del programador garantizar que el programa se ejecute de manera determinista. La sincronización y otros temas de la programación concurrente son bastante complejos y, a menudo, requieren un conocimiento extenso sobre múltiples capas de características de software y hardware en los sistemas informáticos modernos.

Por lo tanto, asumiremos algunos conocimientos previos de estos conceptos mientras cubrimos una parte muy pequeña del tema de sincronización en este artículo. Es decir, introduciremos el concepto de exclusión mutua, también conocido como mutex (a menudo, se da el mismo nombre a los nombres de los objetos en los lenguajes de programación, por ejemplo, std::mutex).

Un mutex es un tipo de mecanismo de bloqueo que puede rodear una sección crítica del programa y garantizar que el acceso a ella esté protegido. Cuando decimos que el recurso compartido está protegido, significa que si un subproceso está realizando la operación de escritura en el objeto compartido, otros subprocesos no operarán hasta que el subproceso anterior finalice la operación.

Tenga en cuenta que un comportamiento como este puede no ser óptimo para algunos problemas, ya que pueden surgir contención de recursos, falta de recursos u otros problemas relacionados con el rendimiento. Por lo tanto, algunos otros mecanismos abordan estos temas y ofrecen diferentes características más allá del alcance de este artículo.

En el siguiente ejemplo, mostramos el uso básico de la clase std::mutex proporcionada por C++ STL. Tenga en cuenta que el soporte estándar para subprocesos se ha agregado desde la versión C++ 11.

Al principio, necesitamos construir un objeto std::mutex que luego se puede usar para controlar el acceso al recurso compartido. std::mutex tiene dos funciones miembro principales: block y desbloquear. La operación de bloqueo se suele llamar antes de que se modifique el recurso compartido y se llama a desbloquear después de la modificación.

El código que se inserta entre estas llamadas se conoce como sección crítica. Aunque el orden anterior del diseño del código es correcto, C++ proporciona otra clase de plantilla útil: std::lock_guard, que puede desbloquear automáticamente el mutex dado cuando se deja el alcance. La razón principal para utilizar lock_guard en lugar de utilizar directamente las funciones miembro lock y unlock es garantizar que el mutex se desbloqueará en todas las rutas de código incluso si se generan las excepciones. Entonces, nuestro ejemplo de código también usará el último método para demostrar el uso de std::mutex.

El programa main está construido para crear dos contenedores vectoriales de enteros aleatorios y luego empujar el contenido de ambos a una lista. La parte complicada es que queremos utilizar varios subprocesos para agregar elementos a la lista.

En realidad, invocamos la función generateNumbers con dos hilos, pero estos operan en diferentes objetos y la sincronización es innecesaria. Una vez que hemos generado enteros, la lista se puede llenar llamando a la función addToList.

Tenga en cuenta que esta función comienza con la construcción lock_guard y luego incluye las operaciones que deben realizarse en la lista. En este caso, solo llama a la función push_back en el objeto de lista dado.

#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;
}

Producción :

1000000, 1000000
list size = 2000000

En el fragmento de código anterior, elegimos crear dos subprocesos separados en cada iteración del bucle for y unirlos al subproceso principal en el mismo bucle. Este escenario es ineficiente ya que se necesita un tiempo de ejecución precioso para crear y destruir subprocesos, pero solo lo ofrecemos para demostrar el uso de mutex.

Por lo general, si es necesario administrar varios subprocesos durante un tiempo arbitrario en el programa, se utiliza el concepto de grupo de subprocesos. La forma más simple de este concepto creará un número fijo de subprocesos al comienzo de la rutina de trabajo y luego comenzará a asignarles unidades de trabajo en forma de cola. Cuando un hilo completa su unidad de trabajo, se puede reutilizar para la siguiente unidad de trabajo pendiente. Sin embargo, tenga en cuenta que nuestro código de controlador solo está diseñado para ser una simple simulación de flujo de trabajo de múltiples subprocesos en un programa.

Autor: 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