Concurrencia y multihilo en C++: desde lo básico hasta los estándares modernos
Las aplicaciones modernas requieren cada vez más la ejecución simultánea de múltiples tareas. Ya sea procesando solicitudes de usuarios en un servidor, renderizando gráficos o trabajando con grandes volúmenes de datos — la concurrencia y el multihilo en C++ se han convertido en habilidades obligatorias para el desarrollador. En este artículo, analizaremos los mecanismos clave disponibles en la biblioteca estándar de C++, desde C++11 hasta C++20. Aprenderá cómo crear hilos, sincronizar su trabajo y evitar errores típicos.
Si ya está familiarizado con los fundamentos de C++, pero desea profundizar en el paralelismo, este material es para usted. No nos adentraremos en la teoría de los sistemas operativos, sino que nos centraremos en las herramientas prácticas: std::thread, std::mutex, std::async y las operaciones atómicas.
1. Fundamentos del multihilo: std::thread y gestión de hilos
En C++11 apareció el soporte estándar para hilos a través de la clase std::thread. Puede crear un nuevo hilo pasando un objeto invocable al constructor: una función, una lambda o un functor.
#include <iostream>#include <thread>
void hello() { std::cout << "Saludo desde el hilo " << std::this_thread::get_id() << std::endl;}
int main() { std::thread t(hello); // Creación del hilo t.join(); // Espera a que el hilo termine return 0;}Es importante llamar siempre a join() o detach() para cada hilo creado; de lo contrario, al destruir el objeto std::thread, el programa finalizará de forma abrupta. join() bloquea el hilo actual hasta que el hilo hijo termine, mientras que detach() permite que el hilo funcione de forma independiente.
Para pasar argumentos a un hilo, utilice parámetros adicionales en el constructor:
#include <thread>#include <string>
void print_sum(int a, int b, const std::string& msg) { std::cout << msg << ": " << (a + b) << std::endl;}
int main() { std::thread t(print_sum, 3, 4, "Suma"); t.join(); return 0;}Tenga en cuenta: los argumentos se copian dentro del hilo. Si necesita pasar una referencia, use std::ref().
2. Sincronización de hilos: mutexes y bloqueos
Al trabajar con datos compartidos desde diferentes hilos, surge la condición de carrera (data race). Para evitarla, se utiliza la sincronización. La herramienta principal es std::mutex.
#include <iostream>#include <thread>#include <mutex>
std::mutex mtx;int shared_counter = 0;
void increment() { for (int i = 0; i < 100000; ++i) { mtx.lock(); ++shared_counter; mtx.unlock(); }}
int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout << "Resultado: " << shared_counter << std::endl; return 0;}Sin embargo, la llamada manual a lock() y unlock() es propensa a errores: si ocurre una excepción entre ellas, el mutex permanecerá bloqueado. Es mejor usar el envoltorio RAII std::lock_guard o std::unique_lock:
void increment() { for (int i = 0; i < 100000; ++i) { std::lock_guard<std::mutex> lock(mtx); ++shared_counter; } // se desbloquea automáticamente al salir del ámbito}Para un control más flexible (por ejemplo, bloqueo condicional), use std::unique_lock. Y si necesita sincronizar varios mutexes sin interbloqueo, aplique std::lock().
3. Tareas asíncronas: std::async y std::future
No siempre es conveniente gestionar los hilos manualmente.