Introducción a la concurrencia y el multihilo en C++
Las aplicaciones modernas requieren el máximo rendimiento. La concurrencia y el multihilo en C++ permiten utilizar eficientemente procesadores multinúcleo, acelerando la ejecución de tareas. Sin comprender estos conceptos, es imposible crear sistemas de alta carga, procesadores de datos o motores de juegos.
En este artículo, analizaremos los mecanismos clave del multihilo en C++, comenzando con std::thread básico y terminando con patrones avanzados. Aprenderá cómo evitar condiciones de carrera, sincronizar hilos correctamente y escribir código paralelo seguro.
Fundamentos del multihilo: std::thread y std::jthread
La clase std::thread (C++11) es la base de la programación multihilo. Permite ejecutar una función o un objeto invocable en un hilo separado. Veamos un ejemplo simple:
#include <iostream>#include <thread>
void worker(int id) { std::cout << "Hilo " << id << " comenzó a trabajar"; std::this_thread::sleep_for(std::chrono::seconds(1)); std::cout << "Hilo " << id << " finalizado";}
int main() { std::thread t1(worker, 1); std::thread t2(worker, 2); t1.join(); t2.join(); return 0;}Puntos clave:
- join() — el hilo principal espera a que el hilo hijo termine
- detach() — el hilo continúa trabajando de forma independiente (riesgo de fuga de recursos)
- std::jthread (C++20) — llama automáticamente a join() en el destructor
Siempre verifique joinable() antes de llamar a join() para evitar una doble unión.
Sincronización de hilos: mutex y bloqueos
El principal problema del multihilo son las condiciones de carrera, cuando varios hilos modifican simultáneamente una variable compartida. Para protegerlas se utilizan mutex.
std::mutex y std::lock_guard
#include <iostream>#include <thread>#include <mutex>
std::mutex mtx;int counter = 0;
void increment() { for (int i = 0; i < 100000; ++i) { std::lock_guard<std::mutex> lock(mtx); ++counter; }}
int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout << "Resultado: " << counter << ""; // 200000 return 0;}std::lock_guard libera automáticamente el mutex al salir del ámbito. Para un control más flexible, use std::unique_lock.
Deadlock y std::scoped_lock
Un bloqueo mutuo (deadlock) ocurre cuando dos hilos se esperan el uno al otro. La solución es usar std::scoped_lock (C++17), que captura múltiples mutex de forma atómica:
std::mutex m1, m2;
void safe_function() { std::scoped_lock lock(m1, m2); // trabajo seguro con dos recursos}Algoritmos paralelos y std::async
Para ejecutar tareas asíncronas sin gestionar hilos manualmente, use std::async. Devuelve un std::future del cual se puede obtener el resultado.
#include <iostream>#include <future>#include <chrono>
int compute(int n) { std::this_thread::sleep_for(std::chrono::seconds(n)); return n * n;}
int main() { auto future1 = std::async(std::launch::async, compute, 2); auto future2 = std::async(std::launch::async, compute, 3); std::cout << "Resultados: " << future1.get() << ", " << future2.get() << ""; // 4, 9 return 0;}Políticas de lanzamiento:
std::launch::async— lanzamiento en un nuevo hilostd::launch::deferred— ejecución diferida (perezosa)