# C++ Multithreading desde cero — Parte 2

## 1\. Introducción

Hasta ahora he abordado los fundamentos conceptuales de la concurrencia: qué es, cuándo conviene usarla y por qué es un elemento esencial en los sistemas modernos. En esta sección daremos el siguiente paso, centrando nuestra atención en la práctica: la gestión de hilos en C++ y su aplicación a través de ejemplos concretos.

Todo programa en C++ comienza con un único hilo: aquel que ejecuta la función `main()`. Sin embargo, la verdadera potencia del lenguaje aparece cuando aprendemos a lanzar nuevos hilos que ejecutan tareas de manera independiente y concurrente. El punto de partida es la clase `std::thread`, introducida en C++11. Esta clase encapsula la creación y control de un hilo de ejecución. En su forma más simple, lanzar un hilo consiste en construir un objeto `std::thread` y especificar qué función se ejecutará en ese nuevo hilo.

***Hello\_Concurrent\_World CD*\_*01:***

```cpp
#include <iostream>
#include <thread>

void hello() {
    std::cout << "Hello Concurrent World\n";
}

int main() {
    std::thread t(hello);  // Lanza un nuevo hilo
    t.join();              // Espera a que el hilo termine
}
```

Este pequeño programa crea dos hilos:

*   el **hilo principal**, que inicia en `main()`,
    
*   y un **hilo secundario**, que comienza ejecutando la función `hello()`.
    

El flujo principal continúa inmediatamente después de lanzar el hilo. Si no esperáramos a que el hilo termine, el programa podría finalizar antes de que el mensaje se muestre. Por eso llamamos a `join()`, que bloquea la ejecución hasta que el hilo finaliza. Aunque el ejemplo parece trivial, marca un cambio profundo: desde este punto, **el control de flujo deja de ser lineal**. Cada hilo representa un camino de ejecución independiente, y depende de nosotros decidir cómo y cuándo sincronizarlos.

* * *

## 2\. Lanzar un hilo

En C++, crear un hilo siempre se reduce a **construir un objeto** `std::thread`, pasando como argumento una función o cualquier objeto *callable*.

Por ejemplo, podríamos usar una función normal:

```cpp
void do_some_work();
std::thread worker(do_some_work);
```

o un objeto que sobrecarga el operador `()`:

```cpp
class background_task {
public:
    void operator()() const {
        do_something();
        do_something_else();
    }
};

background_task task;
std::thread worker(task);
```

Cuando se crea el objeto `std::thread`, su constructor copia la función u objeto *callable* dentro del contexto del nuevo hilo y comienza su ejecución inmediatamente. Una vez que el hilo inicia, su función de entrada se ejecutará hasta completarse, momento en el cual el hilo finalizará de forma automática.

### Riesgos iniciales

Lanzar un hilo puede parecer una operación simple, pero implica un cambio fundamental en la gestión de recursos y en el tiempo de vida de los datos. Si el programa termina o un objeto local se destruye antes de que el hilo haya terminado de usarlo, se entra en terreno de **comportamiento indefinido**.

En particular, si no esperas a que el hilo finalice (por ejemplo, al no llamar a `join()`, debes asegurarte de que los datos a los que accede el hilo sigan siendo válidos hasta que este complete su ejecución.

Este problema no es exclusivo del código concurrente: incluso en un solo hilo es incorrecto acceder a un objeto destruido. Sin embargo, con múltiples hilos, la posibilidad de error se amplifica, porque el momento exacto en que el hilo accede a la memoria depende del planificador del sistema operativo.

Considera el siguiente ejemplo:

```cpp
struct func {
    int& i;
    func(int& i_) : i(i_) {}

    void operator()() {
        for (unsigned j = 0; j < 1000000; ++j) {
            do_something(i);
        }
    }
};

void oops() {
    int some_local_state = 0;
    func my_func(some_local_state);
    std::thread my_thread(my_func);
    my_thread.detach();  // No esperamos a que termine
}
```

Aquí el hilo creado con `my_thread` continúa ejecutándose incluso después de que la función `oops()` haya retornado, el objeto `some_local_state` deja de existir al salir de la función, pero el hilo aún podría estar ejecutando `do_something(i)`, accediendo a una referencia colgante, esto constituye un acceso a memoria destruida, lo que provoca un comportamiento indefinido: desde fallos silenciosos hasta bloqueos o corrupciones de datos.

La situación es similar a mantener un puntero o referencia a una variable local fuera de su ámbito, pero en programación concurrente el error es más fácil de pasar por alto, porque el hilo podría seguir corriendo en segundo plano sin que sea evidente.

Una manera segura de evitar este tipo de errores es:

*   Hacer que la función del hilo sea **autosuficiente**, copiando los datos que necesita en lugar de referenciarlos.
    
*   Asegurarse de que el hilo haya completado su ejecución antes de que los recursos que usa sean destruidos, normalmente mediante una llamada a `join()`.
    

* * *

## 3\. Estado *joinable* y ciclo de vida del hilo

Cada objeto `std::thread` mantiene una asociación con un hilo real del sistema operativo. Mientras esa asociación exista, el hilo se considera **joinable**, es decir, puede ser esperado o desacoplado.

Cuando se llama a `join()`, suceden dos cosas:

1.  El hilo que hace la llamada se bloquea hasta que el hilo asociado termina.
    
2.  Los recursos del sistema utilizados por el hilo son liberados, y el objeto `std::thread` deja de estar asociado a ningún hilo.
    

Después de esa llamada, el hilo **ya no es joinable**, y cualquier intento posterior de llamar a `join()` sobre él producirá un error en tiempo de ejecución.

Puedes verificar este estado con el método `joinable()`:

```cpp
std::thread worker(do_some_work);
if (worker.joinable()) {
    worker.join();
}
```

Por lo tanto, cada hilo debe terminar de una de dos formas:

*   **sincronizándose** mediante `join()`
    
*   **liberándose** mediante `detach()`, que lo convierte en un **hilo en segundo plano** (*background thread*).
    

### Hilos en segundo plano

El método `detach()` permite que un hilo se ejecute **de manera independiente**, sin necesidad de que el hilo principal espere su finalización. Al llamarlo, el hilo queda completamente desacoplado del objeto `std::thread`, pasando a ser gestionado por el sistema operativo.

***background\_task\_CD\_02***

```cpp
void background_task() {
    std::cout << "Tarea en segundo plano iniciada\n";
    std::this_thread::sleep_for(std::chrono::seconds(3));
    std::cout << "Tarea en segundo plano finalizada\n";
}

int main() {
    std::thread t(background_task);
    t.detach();  // El hilo continúa ejecutándose de forma independiente
    std::cout << "Hilo principal continúa sin esperar\n";
}
```

Este enfoque resulta útil cuando la tarea es autónoma —por ejemplo, registrar información, limpiar recursos o realizar un trabajo no crítico— y su finalización no afecta al flujo principal del programa.

Sin embargo, el uso de `detach()` implica una **pérdida total de control** sobre el hilo: ya no puede ser sincronizado, y si accede a recursos locales que ya fueron destruidos, el comportamiento será indefinido. Por ello, debe usarse con precaución y únicamente cuando el hilo no dependa de datos cuyo ciclo de vida sea más corto que el suyo.

### Esperas más precisas

El método `join()` ofrece una sincronización binaria: esperas completamente o no esperas en absoluto. Si necesitas comprobar periódicamente si el hilo ha terminado, o limitar el tiempo de espera, deberás recurrir a mecanismos más avanzados como **futures**, **promises** o **condition variables**, que permiten un control más granular sobre la sincronización.

Estos temas se abordarán más adelante, pero por ahora basta con entender que `join()` es la forma más directa y segura de garantizar que un hilo haya finalizado antes de continuar, mientras que `detach()` ofrece una ejecución completamente independiente, con la responsabilidad adicional de asegurar que los recursos del hilo sigan siendo válidos durante su ejecución.

## 4\. Esperar en circunstancias de excepciones

Gestionar correctamente los hilos no solo implica saber cuándo sincronizarlos, sino también **asegurar que siempre sean liberados**, incluso si ocurre una excepción. Recordemos que un objeto `std::thread` debe terminar su ciclo de vida habiendo sido **unido (**`join()`) o **desacoplado (**`detach()`).  
De lo contrario, al destruirse un objeto `std::thread` todavía *joinable*, el programa llamará a `std::terminate()`.

Esto representa un riesgo evidente en contextos donde el flujo puede interrumpirse abruptamente, por ejemplo, debido a una excepción. Si el control abandona una función antes de ejecutar la llamada a `join()`, el hilo quedará sin gestionar y el programa fallará.

#### Ejemplo del problema

Supón el siguiente escenario simplificado:

```cpp
void f() {
    int some_local_state = 0;
    func my_func(some_local_state);
    std::thread t(my_func);

    do_something_in_current_thread();
    t.join();
}
```

En condiciones normales, este código funciona correctamente: el hilo se une antes de salir de la función.

Pero si `do_something_in_current_thread()` lanza una excepción, **la llamada a** `join()` nunca se ejecutará, y cuando `t` se destruya, el programa terminará con `std::terminate()`.

#### Manejo explícito con `try`/`catch`

Una solución inmediata consiste en usar un bloque `try`/`catch` para garantizar que el hilo se una tanto en la ejecución normal como en la excepcional:

```cpp
void f() {
    int some_local_state = 0;
    func my_func(some_local_state);
    std::thread t(my_func);

    try {
        do_something_in_current_thread();
    } catch (...) {
        t.join();  // Asegura que el hilo se libere
        throw;     // Repropaga la excepción
    }

    t.join();  // Camino normal
}
```

Esta estrategia es funcional, pero tiene dos inconvenientes:

*   Duplica la llamada a `join()`, lo que ensucia el código.
    
*   Es propensa a errores si el bloque `try` no cubre todas las rutas de salida posibles.
    

#### Solución RAII clásica: el patrón *thread guard*

En *C++ Concurrency in Action*, Anthony Williams propone una solución basada en **RAII (Resource Acquisition Is Initialization)** mediante una clase llamada `thread_guard`. Su destructor garantiza que el hilo se una automáticamente al salir del alcance, incluso si se lanza una excepción.

```cpp
class thread_guard {
    std::thread& t;
public:
    explicit thread_guard(std::thread& t_) : t(t_) {}
    ~thread_guard() {
        if (t.joinable())
            t.join();
    }
    thread_guard(thread_guard const&) = delete;
    thread_guard& operator=(thread_guard const&) = delete;
};
```

El principio es simple: cuando el objeto `thread_guard` sale de ámbito —ya sea por una salida normal o por una excepción— su destructor invoca `join()` si el hilo sigue activo. Este patrón es una aplicación directa del RAII y resulta muy eficaz para C++11 y C++17.

* * *

## 5\. std::jthread

En C++20, el estándar introdujo `std::jthread`, que **ya implementa automáticamente la lógica de unión en su destructor**, reemplazando la necesidad de un `thread_guard` manual. Al destruirse un `std::jthread`, si el hilo sigue ejecutándose, el destructor llama a `join()` de manera segura, eliminando el riesgo de `std::terminate()`.

Además, `std::jthread` admite cancelación cooperativa mediante `std::stop_token`, lo que facilita el diseño de tareas que pueden detenerse desde fuera del hilo.

#### Ejemplo con `std::jthread`

```cpp
#include <iostream>
#include <thread>
#include <chrono>

void do_work() {
    std::cout << "Hilo iniciado\n";
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "Hilo finalizado\n";
}

void f() {
    std::jthread t(do_work);  // Se unirá automáticamente al salir del scope
    do_something_in_current_thread();  // Si lanza, no hay problema
}
```

Si `do_something_in_current_thread()` lanza una excepción, el hilo `t` **se unirá automáticamente** al salir del ámbito de la función, gracias al destructor de `std::jthread`.  
Esto elimina por completo la necesidad de un bloque `try` o de una clase auxiliar.

#### Ejemplo con cancelación cooperativa

***stop\_token\_CD\_03***

```cpp
#include <iostream>
#include <thread>
#include <chrono>

void task(std::stop_token st) {
    while (!st.stop_requested()) {
        std::cout << "Trabajando...\n";
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }
    std::cout << "Cancelado\n";
}

int main() {
    std::jthread t(task);  // hilo con soporte de cancelación
    std::this_thread::sleep_for(std::chrono::seconds(2));
    t.request_stop();      // solicita la detención
}
```

El hilo se ejecuta mientras no se solicite su detención, y al salir del `main()`, el destructor de `std::jthread` realiza el `join()` automáticamente, garantizando una finalización limpia.

Hasta este punto se han mostrado las bases para lanzar hilos y aplicar una forma sencilla de sincronización en C++. El trabajo con hilos empieza a adquirir estructura: ya no se trata solo de ejecutar tareas, sino de comprender ciclo de vida del hilo y mantener el control sobre cada una de ellos. En la siguiente parte se profundizaré en la gestión de los hilos, el paso de argumentos y el manejo de su propiedad.

%[https://github.com/Nobody-1321/multithreading_cpp]
