Introducción a std::thread
Antes de la llegada del estándar C++11, el lenguaje no ofrecía soporte nativo para la programación multihilo, obligando a los desarrolladores a depender de las APIs específicas del sistema operativo (como POSIX threads o la API de Windows). Con C++11, se integró una biblioteca estándar que permite gestionar hilos, proteger datos compartidos y realizar operaciones atómicas de forma portable.
El componente principal es std::thread. Al crear un objeto de este tipo, se inicia un nuevo hilo de ejecución. Es fundamental gestionar el ciclo de vida del hilo mediante join() o detach().
#include <iostream>
#include <thread>
void tareaSimple() {
std::cout << "Ejecutando tarea en hilo secundario" << std::endl;
}
int main() {
std::thread hiloPrincipal(tareaSimple);
hiloPrincipal.join(); // Bloquea el hilo principal hasta que tareaSimple finalice
return 0;
}
Si un objeto std::thread se destruye mientras el hilo asociado sigue activo (sin haber llamado a join() ni a detach()), el programa invocará std::terminate y se cerrará inesperadamente. Mientras que join() espera la finalización, detach() desvincula el hilo, permitiéndole ejecutarse de forma independiente en segundo plano (daemon thread). Sin embargo, el uso de detach() es menos común debido a la dificultad para controlar la liberación de recursos.
Propiedades y transferencia de hilos
Los hilos en C++ no se pueden copiar, pero sí se pueden mover. Esto permite transferir la propiedad de un hilo de ejecución entre diferentes objetos std::thread.
#include <iostream>
#include <thread>
#include <utility>
void proceso() {
std::cout << "Hilo en movimiento" << std::endl;
}
int main() {
std::thread t_origen(proceso);
std::thread t_destino = std::move(t_origen); // Transferencia de propiedad
if (t_destino.joinable()) {
t_destino.join();
}
return 0;
}
Además, es posible obtener un identificadro único para cada hilo mediante get_id(), lo cual es útil para depuración o para aplicar lógicas específicas según el hilo que esté ejeuctando el código.
Uso de std::mutex para la protección de datos
Cuando múltiples hilos acceden a un recurso compartido, se producen condiciones de carrera. Para evitar esto, C++11 introduce varios tipos de mutex (exclusión mutua). El más utilizado es std::mutex.
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
std::mutex mtx_consola;
void imprimirSeguro(int id) {
std::lock_guard<std::mutex> bloqueo(mtx_consola);
// lock_guard aplica RAII: bloquea en el constructor y libera en el destructor
std::cout << "Hilo ID: " << id << " accediendo al recurso" << std::endl;
}
int main() {
std::vector<std::thread> hilos;
for (int i = 0; i < 5; ++i) {
hilos.emplace_back(imprimirSeguro, i);
}
for (auto& h : hilos) {
h.join();
}
return 0;
}
Aunque se puede usar lock() y unlock() manualmente, se recomienda std::lock_guard o std::unique_lock. Estos envoltorios siguen el patrón RAII, asegurando que el mutex se libere incluso si ocurre una excepción dentro de la sección crítica.
Sincronización con std::condition_variable
Las variables de condición permiten que un hilo se bloquee hasta que otro hilo le notifique que se ha cumplido una condición específica. Esto es esencial para implementar patrones como el de Productor-Consumidor.
#include <iostream>
#include <condition_variable>
#include <thread>
#include <queue>
std::queue<int> cola_datos;
std::mutex mtx_cola;
std::condition_variable cv_flujo;
void consumidor() {
while (true) {
std::unique_lock<std::mutex> lck(mtx_cola);
cv_flujo.wait(lck, []{ return !cola_datos.empty(); }); // Espera condicionada
int valor = cola_datos.front();
cola_datos.pop();
lck.unlock();
std::cout << "Procesado: " << valor << std::endl;
if (valor == -1) break; // Condición de salida
}
}
void productor() {
for (int i = 0; i < 5; ++i) {
{
std::lock_guard<std::mutex> lck(mtx_cola);
cola_datos.push(i);
}
cv_flujo.notify_one(); // Notifica al hilo que espera
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
{
std::lock_guard<std::mutex> lck(mtx_cola);
cola_datos.push(-1);
}
cv_flujo.notify_one();
}
int main() {
std::thread t1(consumidor);
std::thread t2(productor);
t1.join();
t2.join();
return 0;
}
Almacenamiento Local de Hilo (thread_local)
La palabra clave thread_local permite definir variables que tienen una instancia independiente por cada hilo. Cada hilo posee su propia copia de la variable, la cual se inicializa al comenzar el hilo y se destruye al finalizar.
#include <iostream>
#include <thread>
thread_local int contadorLocal = 0;
void incrementar(int pasos) {
for(int i = 0; i < pasos; ++i) {
contadorLocal++;
}
std::cout << "Hilo " << std::this_thread::get_id() << " - Contador: " << contadorLocal << std::endl;
}
int main() {
std::thread h1(incrementar, 10);
std::thread h2(incrementar, 50);
h1.join();
h2.join();
return 0;
}
En el ejemplo anterior, aunque contadorLocal parece una variable global, el cambio realizado en un hilo no afecta al valor de la variable en el otro hilo.