Gestión de Concurrencia en C++: Hilos, Mutexes y Variables de Condición

Introducción a la Concurrencia en C++

La programación concurrente permite a un programa ejecutar múltiples tareas de forma simultánea, mejorando la capacidad de respuesta y el rendimiento en sistemas con múltiples núcleos de procesamiento. Tradicionalmente, incluso con un solo núcleo, los sistemas operativos simulaban la concurrencia cambiando rápidamente entre tareas. Con la llegada de los procesadores multinúcleo, la verdadera ejecución paralela es posible, permitiendo que diferentes tareas se ejecuten físicamente al mismo tiempo.

En el ámbito de la ejecución de programas:

  • Un programa ejecutable es un archivo en disco listo para ser lanzado.
  • Un proceso es una instancia en ejecución de un programa. Cada proceso tiene su propio espacio de memoria aislado y recursos del sistema.
  • Un hilo (thread) es una unidad de ejecución dentro de un proceso. Cada proceso inicia con un hilo principal (que ejecuta, por ejemplo, la función main), y puede crear hilos adicionales para realizar tareas concurrentes. Los hilos dentro del mismo proceso comparten el espacio de memoria y recursos, lo que facilita la comunicación pero introduce desafíos de sincronización. Aunque los hilos son más ligeros y rápidos de crear que los procesos, un exceso de hilos puede llevar a una sobrecarga en la gestión del sistema y la conmutación de contexto.

C++11 introdujo una biblioteca estándar para el manejo de hilos (<thread>), lo que permite escribir código concurrente portátil a través de diferentes plataformas.

Gestión de Hilos con std::thread

La clase std::thread es la herramienta fundamental en C++ para crear y gestionar hilos de ejecución. Un hilo se inicia pasando una función o un objeto invocable a su constructor. La función del hilo comenzará a ejecutarse en un nuevo hilo tan pronto como el objeto std::thread sea construido.

join(): Esperar la Finalización de un Hilo

El método join() bloquea el hilo llamador (típicamente el hilo principal) hasta que el hilo asociado al objeto std::thread haya completado su ejecución. Es una práctica común usar join() para aseguarr que todos los hilos secundarios terminen antes de que el hilo principal finalice, evitando que el programa se cierre prematuramente mientras los hilos secundarios aún están en ejecución.

#include <iostream>
#include <thread>
#include <chrono> // Para std::chrono::milliseconds
#include <vector> // Para std::vector

// Función que será ejecutada por un hilo
void worker_routine(int id) {
    std::cout << "Hilo de trabajo " << id << ": Iniciando..." << std::endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(500)); // Simula alguna tarea
    std::cout << "Hilo de trabajo " << id << ": Finalizando." << std::endl;
}

int main() {
    std::cout << "Hilo principal: Lanzando hilo de trabajo 101." << std::endl;
    std::thread t1(worker_routine, 101); // Crea y lanza el hilo t1

    // El hilo principal puede realizar otras tareas concurrentemente
    std::cout << "Hilo principal: Realizando trabajo concurrente." << std::endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(200));

    t1.join(); // Bloquea el hilo principal hasta que t1 termine
    std::cout << "Hilo principal: Hilo 101 ha terminado y se ha unido." << std::endl;

    // Gestión de múltiples hilos en un vector
    std::cout << "\nHilo principal: Lanzando 3 hilos de trabajo desde un vector." << std::endl;
    std::vector<std::thread> thread_pool;
    for (int i = 0; i < 3; ++i) {
        thread_pool.emplace_back(worker_routine, 200 + i); // Añade hilos al vector
    }

    std::cout << "Hilo principal: Esperando la finalización de todos los hilos del pool." << std::endl;
    for (std::thread& t : thread_pool) {
        if (t.joinable()) { // Comprueba si el hilo puede ser unido
            t.join();
        }
    }
    std::cout << "Hilo principal: Todos los hilos del pool han finalizado." << std::endl;

    return 0;
}

detach(): Hilos en Segundo Plano

El método detach() permite que un hilo continúe su ejecución de forma independiente del objeto std::thread que lo lanzó. Una vez detachado, el objeto std::thread pierde su asociación con el hilo subyacente, y este último se convierte en un "hilo demonio" gestionado por el entorno de ejecución de C++. El hilo principal puede terminar sin esperar a los hilos detachados.

Advertencia: Peligro de referencias y punteros colgantes. Cuando un hilo es detachado, cualquier dato al que acceda por referencia o puntero debe tener una vida útil garantizada más allá de la duración del hilo principal. Si el hilo principal termina y los datos a los que el hilo detachado hace referencia salen de ámbito y son destruidos, el hilo detachado intentará acceder a memoria inválida, causando un comportamiento indefinido.

#include <iostream>
#include <thread>
#include <string>
#include <chrono>
#include <utility> // Para std::move

// Clase invocable (functor) para tareas de hilo
class TaskExecutor {
private:
    std::string task_name;
    int current_value; // Se pasa por valor para seguridad con detach
public:
    TaskExecutor(const std::string& name, int start_val) : task_name(name), current_value(start_val) {
        std::cout << "TaskExecutor Creado: " << task_name << std::endl;
    }

    // Constructor de movimiento para eficiencia (std::thread constructor lo prefiere)
    TaskExecutor(TaskExecutor&& other) noexcept
        : task_name(std::move(other.task_name)), current_value(other.current_value) {
        std::cout << "TaskExecutor Movido: " << task_name << std::endl;
    }

    void operator()() {
        for (int i = 0; i < 3; ++i) {
            std::cout << "Hilo " << std::this_thread::get_id() << " [" << task_name << "]: Valor " << current_value++ << std::endl;
            std::this_thread::sleep_for(std::chrono::milliseconds(150));
        }
    }
};

void lambda_thread_task(int iteration_count) {
    for (int i = 0; i < iteration_count; ++i) {
        std::cout << "Lambda Hilo " << std::this_thread::get_id() << ": Iteración " << i + 1 << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

int main() {
    std::cout << "--- Hilos Detachados (Segundo Plano) ---" << std::endl;

    // 1. Hilo detachado con expresión lambda
    std::thread background_lambda(lambda_thread_task, 4);
    background_lambda.detach(); // El hilo continúa ejecutándose independientemente
    std::cout << "Hilo principal: Hilo lambda detachado. Es joinable? "
              << (background_lambda.joinable() ? "Sí" : "No") << std::endl;

    // 2. Hilo detachado con objeto invocable (functor)
    // Se mueve el objeto `TaskExecutor` al hilo para que este tenga la propiedad completa de sus datos.
    TaskExecutor background_executor("Tarea persistente", 200);
    std::thread background_task(std::move(background_executor)); // Mueve el functor
    background_task.detach(); // Hilo de fondo detachado
    std::cout << "Hilo principal: Hilo functor detachado. Es joinable? "
              << (background_task.joinable() ? "Sí" : "No") << std::endl;

    // El hilo principal puede terminar antes que los hilos detachados.
    // Damos un tiempo para que los hilos de fondo hagan algo de trabajo.
    std::cout << "Hilo principal: Finalizando rápidamente..." << std::endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(700)); // Breve pausa

    std::cout << "Hilo principal: Finalizado. Los hilos detachados pueden seguir ejecutándose." << std::endl;

    // Al finalizar main(), los hilos detachados son gestionados por el entorno de ejecución de C++.
    // Es crucial que no accedan a recursos que salgan de ámbito al terminar main().

    return 0;
}

joinable(): Comprobar el Estado de un Hilo

El método joinable() devuelve true si el objeto std::thread representa un hilo de ejecución activo que puede ser unido (join()) o detachado (detach()). Una vez que join() o detach() se han llamado, el hilo ya no es joinable. Un objeto std::thread recién construido sin función (std::thread()) tampoco es joinable.

Sincronización y Protección de Datos Compartidos

Cuando múltiples hilos acceden y modifican los mismos datos compartidos, pueden surgir problemas de consistencia conocidos como condiciones de carrera (race conditions). Para evitar estos problemas, es esencial sincronizar el acceso a los datos compartidos. C++ proporciona varias herramientas para esto, siendo los mutexes y las variables de condición las principales.

Mutexes (Objetos de Exclusión Mutua)

Un mutex es un mecanismo de bloqueo que garantiza que solo un hilo pueda acceder a una sección crítica de código (aquella que manipula datos compartidos) a la vez. El hilo que adquiere el bloqueo de un mutex tiene acceso exclusivo a los datos protegidos hasta que lo libera.

  • std::mutex: El tipo de mutex más básico.
  • lock(): Bloquea el mutex. Si ya está bloqueado, el hilo se bloqueará hasta que esté disponible.
  • unlock(): Desbloquea el mutex.

El uso directo de lock() y unlock() puede ser propenso a errores (olvidar desbloquear, desbloquear en rutas de error, etc.). Por ello, se prefieren los patrones RAII (Resource Acquisition Is Initialization).

std::lock_guard: Protección RAII Básica

std::lock_guard es una plantilla de clase que implementa el patrón RAII para mutexes. Adquiere un bloqueo sobre un mutex en su constructor y lo libera automáticamente en su destructor (cuando sale del ámbito). Esto garantiza que el mutex siempre se desbloquee, incluso si ocurre una excepción.

std::unique_lock: Protección RAII Flexible

std::unique_lock es una plantilla de clase más flexible que std::lock_guard. También sigue el patrón RAII, pero ofrece funcionalidades adicionales:

  • Bloqueo Diferido (std::defer_lock): Crea un std::unique_lock sin bloquear el mutex inmediatamente. El bloqueo se puede adquirir más tarde llamando a lock().
  • Intento de Bloqueo (std::try_to_lock): Intenta bloquear el mutex. Si tiene éxito, adquiere el bloqueo; de lo contrario, no bloquea y permite al hilo continuar. Útil para evitar el bloqueo indefinido o realizar otras tareas.
  • Adopción de Bloqueo (std::adopt_lock): Asume la propiedad de un mutex que ya ha sido bloqueado por el hilo actual (por ejemplo, con std::lock()).
  • Desbloqueo Explícito (unlock()): Permite desbloquear el mutex antes de que el std::unique_lock salga de ámbito. Útil si la sección crítica es muy pequeña y se desea liberar el recurso antes.
  • Transferencia de Propiedad (release()): Permite transferir la propiedad del mutex a otro std::unique_lock o a un puntero.

Interbloqueos (Deadlocks) y su Solución

Un interbloqueo ocurre cuando dos o más hilos quedan bloqueados indefinidamente, esperando un recurso que está en posesión de otro hilo bloqueado. Esto sucede a menudo cuando se intentan adquirir múltiples mutexes en órdenes inconsistentes.

Ejemplo de interbloqueo:

  • El Hilo A bloquea el Mutex 1, luego intenta bloquear el Mutex 2.
  • El Hilo B bloquea el Mutex 2, luego intenta bloquear el Mutex 1.

Si la secuencia de eventos es tal que el Hilo A adquiere el Mutex 1 y el Hilo B adquiere el Mutex 2, ambos hilos quedarán bloqueados esperando el mutex que el otro posee.

Soluciones comunes para evitar interbloqueos:

  1. Orden Consistente de Bloqueo: Siempre adquirir los mutexes en el mismo orden en todos los hilos.
  2. std::lock(): Esta función puede bloquear dos o más mutexes a la vez de forma atómica. Garantiza que todos los mutexes sean adquiridos con éxito, o ninguno lo sea, evitando el interbloqueo. Se usa típicamente con std::unique_lock y std::adopt_lock.
#include <iostream>
#include <thread>
#include <mutex>
#include <list>
#include <chrono>
#include <utility> // Para std::ref

class MessageQueue {
private:
    std::list<int> messages; // Recurso compartido: cola de mensajes
    std::mutex queue_mtx;    // Mutex para la cola
    std::mutex log_mtx;      // Mutex para la salida a consola (recurso adicional)

public:
    void enqueue_message(int msg_id) {
        // Protección básica con std::lock_guard
        std::lock_guard<std::mutex> guard_q(queue_mtx);
        messages.push_back(msg_id);
        
        std::lock_guard<std::mutex> guard_l(log_mtx);
        std::cout << "Productor " << std::this_thread::get_id() << ": Añadido " << msg_id << std::endl;
    }

    bool dequeue_message(int& msg_id) {
        // Protección flexible con std::unique_lock
        std::unique_lock<std::mutex> u_lock_q(queue_mtx);

        if (!messages.empty()) {
            msg_id = messages.front();
            messages.pop_front();
            u_lock_q.unlock(); // Desbloqueo temprano si la sección crítica es corta

            std::lock_guard<std::mutex> guard_l(log_mtx);
            std::cout << "Consumidor " << std::this_thread::get_id() << ": Extraído " << msg_id << std::endl;
            return true;
        }
        return false;
    }

    // Demostración de interbloqueo potencial con órdenes de bloqueo inconsistentes
    void access_resources_order1(int thread_idx) {
        std::lock_guard<std::mutex> guard_l(log_mtx);
        std::cout << "Hilo " << thread_idx << ": Intentando adquirir recursos en Orden 1 (Cola, Log)..." << std::endl;
        
        queue_mtx.lock(); // Bloquea Mutex 1
        std::this_thread::sleep_for(std::chrono::milliseconds(50)); // Simula trabajo
        
        // ¡Potencial de interbloqueo aquí si otro hilo tiene log_mtx!
        log_mtx.lock();    // Bloquea Mutex 2
        std::cout << "Hilo " << thread_idx << ": Ambos mutexes adquiridos en Orden 1." << std::endl;
        
        // Operación...
        log_mtx.unlock();
        queue_mtx.unlock();
        std::cout << "Hilo " << thread_idx << ": Ambos mutexes liberados en Orden 1." << std::endl;
    }

    void access_resources_order2(int thread_idx) {
        std::lock_guard<std::mutex> guard_l(log_mtx);
        std::cout << "Hilo " << thread_idx << ": Intentando adquirir recursos en Orden 2 (Log, Cola)..." << std::endl;
        
        log_mtx.lock();    // Bloquea Mutex 2
        std::this_thread::sleep_for(std::chrono::milliseconds(50)); // Simula trabajo
        
        // ¡Potencial de interbloqueo aquí si otro hilo tiene queue_mtx!
        queue_mtx.lock(); // Bloquea Mutex 1
        std::cout << "Hilo " << thread_idx << ": Ambos mutexes adquiridos en Orden 2." << std::endl;
        
        // Operación...
        queue_mtx.unlock();
        log_mtx.unlock();
        std::cout << "Hilo " << thread_idx << ": Ambos mutexes liberados en Orden 2." << std::endl;
    }

    // Solución al interbloqueo con std::lock y std::unique_lock::adopt_lock
    void access_resources_deadlock_safe(int thread_idx) {
        std::lock_guard<std::mutex> print_guard(log_mtx);
        std::cout << "Hilo " << thread_idx << ": Usando std::lock para adquirir recursos de forma segura..." << std::endl;
        
        // std::lock adquiere todos los mutexes de forma atómica
        std::lock(queue_mtx, log_mtx); 
        
        // std::unique_lock con std::adopt_lock para gestionar los mutexes ya bloqueados
        std::unique_lock<std::mutex> ul_queue(queue_mtx, std::adopt_lock);
        std::unique_lock<std::mutex> ul_log(log_mtx, std::adopt_lock);
        
        std::cout << "Hilo " << thread_idx << ": Ambos mutexes adquiridos de forma segura." << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(50)); // Simular operación
        std::cout << "Hilo " << thread_idx << ": Operación segura completada, mutexes liberados." << std::endl;
    }

    // Demostración de try_to_lock
    void try_enqueue_message(int msg_id) {
        std::unique_lock<std::mutex> u_lock(queue_mtx, std::try_to_lock);
        if (u_lock.owns_lock()) { // owns_lock() devuelve true si el bloqueo fue exitoso
            messages.push_back(msg_id);
            std::lock_guard<std::mutex> guard_l(log_mtx);
            std::cout << "Hilo " << std::this_thread::get_id() << ": Mensaje " << msg_id << " añadido (try_lock exitoso)." << std::endl;
        } else {
            std::lock_guard<std::mutex> guard_l(log_mtx);
            std::cout << "Hilo " << std::this_thread::get_id() << ": No se pudo añadir " << msg_id << " (try_lock fallido), haciendo otra cosa." << std::endl;
        }
    }
};

void producer_task(MessageQueue& mq, int start_val, int count) {
    for (int i = 0; i < count; ++i) {
        mq.enqueue_message(start_val + i);
        std::this_thread::sleep_for(std::chrono::milliseconds(5));
    }
}

void consumer_task(MessageQueue& mq, int consumer_id, int count) {
    int message;
    for (int i = 0; i < count; ++i) {
        if (!mq.dequeue_message(message)) {
            std::this_thread::sleep_for(std::chrono::milliseconds(10)); // Esperar si la cola está vacía
        }
    }
}

int main() {
    MessageQueue mq;

    std::cout << "\n--- Protección Básica de Cola de Mensajes ---" << std::endl;
    std::thread prod1(producer_task, std::ref(mq), 1000, 5);
    std::thread cons1(consumer_task, std::ref(mq), 1, 3);

    prod1.join();
    cons1.join();
    
    std::cout << "\n--- Demostración de std::unique_lock con try_to_lock ---" << std::endl;
    std::vector<std::thread> try_threads;
    for(int i=0; i<5; ++i) {
        try_threads.emplace_back(&MessageQueue::try_enqueue_message, &mq, i + 500);
    }
    for(auto& t : try_threads) t.join();

    std::cout << "\n--- Escenario de Interbloqueo Potencial ---" << std::endl;
    std::thread dl_t1(&MessageQueue::access_resources_order1, &mq, 1);
    std::thread dl_t2(&MessageQueue::access_resources_order2, &mq, 2);
    // Estos hilos tienen el potencial de interbloquearse. La ejecución puede colgar.
    dl_t1.join(); 
    dl_t2.join();
    std::cout << "Demostración de interbloqueo finalizada (puede haber colgado)." << std::endl;

    std::cout << "\n--- Solución de Interbloqueo con std::lock ---" << std::endl;
    std::thread safe_t1(&MessageQueue::access_resources_deadlock_safe, std::ref(mq), 3);
    std::thread safe_t2(&MessageQueue::access_resources_deadlock_safe, std::ref(mq), 4);
    safe_t1.join();
    safe_t2.join();
    std::cout << "Solución de interbloqueo con std::lock finalizada." << std::endl;

    return 0;
}

Patrón Singleton y std::call_once

El patrón de diseño Singleton garantiza que una clase tenga solo una instancia en todo el programa y proporciona un punto de acceso global a ella. En un entorno multihilo, la inicialización del Singleton debe ser thread-safe para evitar la creación de múltiples instancias si varios hilos intentan acceder a ella simultáneamente.

Inicialización Thread-Safe de Singleton

C++11 garantiza que la inicialización de variables estáticas locales es thread-safe. Este es el método más simple y recomendado para implementar un Singleton thread-safe en C++ moderno:

#include <iostream>
#include <thread>
#include <mutex> // Para std::once_flag, std::call_once
#include <chrono>

// Singleton implementado con variable estática local (thread-safe C++11+)
class AppLogger {
private:
    AppLogger() {
        std::cout << "AppLogger: Inicializando el sistema de registro." << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(200)); // Simula inicialización costosa
    }

    AppLogger(const AppLogger&) = delete; // Deshabilitar copia
    AppLogger& operator=(const AppLogger&) = delete; // Deshabilitar asignación

public:
    static AppLogger& get_instance() {
        // La inicialización de 'instance' está garantizada como thread-safe por el estándar.
        static AppLogger instance; 
        return instance;
    }

    void log_message(const std::string& msg) {
        std::cout << "Log [" << std::this_thread::get_id() << "]: " << msg << std::endl;
    }
};

// Función de trabajo para los hilos que usan el logger
void logger_worker(int id) {
    AppLogger& logger = AppLogger::get_instance();
    logger.log_message("Worker " + std::to_string(id) + " inició sesión.");
    std::this_thread::sleep_for(std::chrono::milliseconds(50));
    logger.log_message("Worker " + std::to_string(id) + " terminó sesión.");
}

int main() {
    std::cout << "--- Demostración de Singleton con Inicialización Thread-Safe ---" << std::endl;

    std::vector<std::thread> logger_threads;
    for (int i = 0; i < 3; ++i) {
        logger_threads.emplace_back(logger_worker, i + 1);
    }

    for (auto& t : logger_threads) {
        t.join();
    }

    std::cout << "\nTodos los hilos han accedido al Singleton AppLogger de forma segura." << std::endl;

    return 0;
}

std::call_once: Ejecutar una Función Exactamente Una Vez

std::call_once es una función que garantiza que una función invocable se ejecute exactamente una vez, incluso si es llamada concurrentemente por múltiples hilos. Es útil cuando la lógica de inicialización es más compleja o está separada de la función que devuelve la instancia del Singleton.

  • Requiere un objeto std::once_flag para rastrear si la función ya ha sido llamada.
#include <iostream>
#include <thread>
#include <mutex> // Para std::once_flag, std::call_once
#include <vector>
#include <chrono>

// Singleton implementado con std::call_once
class ConfigurationManager {
private:
    ConfigurationManager() {
        std::cout << "ConfigurationManager: Cargando configuración desde disco..." << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Simula carga
    }
    ConfigurationManager(const ConfigurationManager&) = delete;
    ConfigurationManager& operator=(const ConfigurationManager&) = delete;

    static ConfigurationManager* s_instance;
    static std::once_flag s_once_flag; // Flag para controlar la llamada única

    // Función auxiliar para crear la instancia (será llamada por std::call_once)
    static void create_instance_internal() {
        s_instance = new ConfigurationManager();
    }

public:
    static ConfigurationManager& get_manager() {
        // Garantiza que create_instance_internal se ejecute exactamente una vez
        std::call_once(s_once_flag, create_instance_internal);
        return *s_instance;
    }

    void load_setting(const std::string& key) {
        std::cout << "Config [" << std::this_thread::get_id() << "]: Obteniendo ajuste '" << key << "'" << std::endl;
    }

    // Función de limpieza para liberar memoria (si se usa 'new' explícitamente)
    static void cleanup() {
        if (s_instance) {
            delete s_instance;
            s_instance = nullptr;
        }
    }
};

// Inicialización de miembros estáticos
ConfigurationManager* ConfigurationManager::s_instance = nullptr;
std::once_flag ConfigurationManager::s_once_flag;

// Función de trabajo para los hilos que usan el gestor de configuración
void config_worker(int id) {
    ConfigurationManager& config = ConfigurationManager::get_manager();
    config.load_setting("Ajuste" + std::to_string(id));
    std::this_thread::sleep_for(std::chrono::milliseconds(30));
    config.load_setting("OtroAjuste" + std::to_string(id));
}

int main() {
    std::cout << "--- Demostración de Singleton con std::call_once ---" << std::endl;

    std::vector<std::thread> config_threads;
    for (int i = 0; i < 3; ++i) {
        config_threads.emplace_back(config_worker, i + 1);
    }

    for (auto& t : config_threads) {
        t.join();
    }
    
    // Es importante llamar a cleanup si el Singleton se crea con 'new'
    ConfigurationManager::cleanup();

    std::cout << "\nTodos los Singletons (ConfigurationManager) han sido accedidos de forma segura." << std::endl;

    return 0;
}

Variables de Condición (std::condition_variable)

Las variables de condición son una herramienta de sincronización más avanzada, utilizadas para la comunicación entre hilos. Permiten que un hilo se bloquee (espere) hasta que una condición específica sea verdadera, mientras que otro hilo puede señalizar (notificar) que la condición ha cambiado. Son fundamentales para implementar patrones como productor-consumidor o barreras.

Una std::condition_variable siempre debe usarse junto con un std::unique_lock<std::mutex>.

wait(): Esperar una Condición

El método wait() hace que el hilo actual se bloquee hasta que la variable de condición sea notificada y, opcionalmente, una condición predicada sea verdadera. Hay dos sobrecargas principales:

  • wait(std::unique_lock<std::mutex>& lock): El hilo libera el bloqueo del mutex, se bloquea y espera una notificación. Cuando es notificado, intenta readquirir el bloqueo. Es responsabilidad del programador verificar la condición después de la reactivación.
  • wait(std::unique_lock<std::mutex>& lock, Predicate pred): Esta es la forma preferida. El hilo libera el bloqueo y espera. Cuando es notificado, readquiere el bloqueo y evalúa el predicado (una función o lambda). Si el predicado es falso, el hilo vuelve a esperar. Si es verdadero, el hilo continúa. Esto maneja automáticamente las "activaciones espurias" ( spurious wakeups), donde un hilo se reactiva sin una notificación real.

notify_one() y notify_all(): Señalizar Cambios

  • notify_one(): Despierta a uno de los hilos que actualmente están esperando en la variable de condición. No hay garantía sobre cuál hilo será despertado.
  • notify_all(): Despierta a todos los hilos que actualmente están esperando en la variable de condición.

Estas funciones deben llamarse cuando la condición por la que esperan otros hilos ha cambiado, generalmente después de modificar los datos compartidos y dentro de la sección crítica (mientras se mantiene el bloqueo del mutex).

Ejemplo: Patrón Productor-Consumidor

Un productor añade elementos a una cola y un consumidor los extrae. La variable de condición asegura que el consumidor no intente extraer de una cola vacía y que el productor no intente añadir a una cola llena (aunque este ejemplo solo maneja la cola vacía).

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <condition_variable>
#include <list>
#include <chrono>
#include <string>

class ThreadSafeQueue {
private:
    std::list<int> data_elements;
    std::mutex mtx;
    std::condition_variable cv_data_available; // Variable de condición

public:
    void enqueue(int item) {
        std::unique_lock<std::mutex> lock(mtx); // Bloquea el mutex
        data_elements.push_back(item);
        std::cout << "Productor " << std::this_thread::get_id() << ": Añadido " << item << ". Tamaño: " << data_elements.size() << std::endl;
        cv_data_available.notify_one(); // Notifica a UN hilo consumidor que hay datos
    }

    int dequeue() {
        std::unique_lock<std::mutex> lock(mtx); // Bloquea el mutex
        
        // Espera hasta que la cola no esté vacía.
        // El predicado (lambda) previene activaciones espurias y verifica la condición
        // después de cada notificación y readquisición del bloqueo.
        cv_data_available.wait(lock, [this]() { return !data_elements.empty(); });

        int item = data_elements.front();
        data_elements.pop_front();
        std::cout << "Consumidor " << std::this_thread::get_id() << ": Extraído " << item << ". Tamaño: " << data_elements.size() << std::endl;
        return item;
    }

    bool is_empty() {
        std::lock_guard<std::mutex> guard(mtx);
        return data_elements.empty();
    }
};

void producer_work(ThreadSafeQueue& queue, int start_val, int num_items) {
    for (int i = 0; i < num_items; ++i) {
        queue.enqueue(start_val + i);
        std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Simula trabajo
    }
}

void consumer_work(ThreadSafeQueue& queue, int consumer_id, int num_to_consume) {
    for (int i = 0; i < num_to_consume; ++i) {
        int item = queue.dequeue(); // Se bloqueará si la cola está vacía hasta que haya un elemento
        //std::cout << "Consumidor ID " << consumer_id << ": Procesando " << item << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(150)); // Simula procesamiento
    }
}

int main() {
    std::cout << "--- Variables de Condición (Patrón Productor-Consumidor) ---" << std::endl;

    ThreadSafeQueue shared_queue;

    // Crear un productor que añade 10 elementos
    std::thread producer(producer_work, std::ref(shared_queue), 1, 10);

    // Crear dos consumidores que intentarán procesar 5 elementos cada uno
    std::thread consumer1(consumer_work, std::ref(shared_queue), 1, 5);
    std::thread consumer2(consumer_work, std::ref(shared_queue), 2, 5);

    producer.join();
    consumer1.join();
    consumer2.join();

    std::cout << "\nProceso Productor-Consumidor finalizado." << std::endl;
    std::cout << "Cola final vacía: " << (shared_queue.is_empty() ? "Sí" : "No") << std::endl;

    return 0;
}

Etiquetas: C++ Multithreading std::thread mutex Condition Variables

Publicado el 7-26 21:56