# C++ Multithreading desde cero — Parte 3

La base del trabajo con hilos ya está establecida: crear un hilo, controlar su finalización y mantener su ejecución dentro de un marco seguro. A partir de aquí, la concurrencia deja de ser solo una cuestión de lanzar tareas en paralelo y empieza a depender de cómo se comunican esas tareas, cómo comparten datos y cómo se transfiere la responsabilidad sobre cada hilo.

Esta sección se centra en esos aspectos prácticos. Se examina el paso de argumentos, la propiedad de los hilos y la organización de múltiples hilos en ejecución. Cada uno de estos elementos define la manera en que un programa concurrente distribuye su trabajo y aprovecha realmente los recursos del sistema.

## Pasar argumentos a una función de hilo

Cuando creamos un hilo en C++ con `std::thread`, además de especificar la función que ejecutará, también podemos pasarle argumentos. Esto se realiza directamente en el constructor del hilo, como si estuviéramos llamando a la función:

```cpp
void f(int i, std::string const& s);

int main() {
    std::thread t(f, 3, "hello");
    t.join();
}
```

En este caso, el hilo ejecutará la llamada `f(3, "hello")`. Sin embargo, aunque parezca una llamada directa, internamente **el constructor de** `std::thread` copia los argumentos a un almacenamiento interno, y luego los pasa a la función **como valores temporales (rvalues)** dentro del nuevo hilo de ejecución.

Esto tiene varias consecuencias importantes.

### Copias internas y conversiones tardías

Los argumentos se copian tal cual son proporcionados, **antes** de que ocurra cualquier conversión de tipo esperada por la función.  
Por ejemplo, en el siguiente código:

```cpp
void f(int i, std::string const& s);

void oops(int some_param) {
    char buffer[1024];
    sprintf(buffer, "%i", some_param);
    std::thread t(f, 3, buffer);  // <- Peligroso
    t.detach();
}
```

Aquí `buffer` es un arreglo local, y lo que realmente se pasa al hilo es un puntero (`char*`). El constructor de `std::thread` copia ese puntero **sin realizar la conversión a** `std::string`, porque esa conversión ocurre más tarde, cuando el hilo comienza su ejecución.  
El problema es que, para cuando el nuevo hilo intenta hacer la conversión, `buffer` podría haber dejado de existir, produciendo **comportamiento indefinido**.

La forma correcta es convertir explícitamente a `std::string` **antes** de pasar el argumento:

```cpp
void not_oops(int some_param) {
    char buffer[1024];
    sprintf(buffer, "%i", some_param);
    std::thread t(f, 3, std::string(buffer));  // <- Correcto
    t.detach();
}
```

En este caso, la conversión a `std::string` ocurre en el hilo principal, de modo que lo que se copia internamente es un objeto completamente válido e independiente.

### Paso por referencia: `std::ref` y `std::cref`

De manera predeterminada, `std::thread` **copia todos los argumentos**, incluso si la función espera una referencia.  
Esto significa que el siguiente código **no compilará**:

```cpp
void update_data_for_widget(widget_id w, widget_data& data);

void oops_again(widget_id w) {
    widget_data data;
    std::thread t(update_data_for_widget, w, data);  // <- Error
    t.join();
}
```

Aquí, `update_data_for_widget` espera una referencia, pero `std::thread` intenta pasar una copia de `data` como si fuera un rvalue, lo cual no es válido para una referencia no constante.

Para indicar explícitamente que queremos pasar una **referencia**, debemos envolver el argumento con `std::ref` (o `std::cref` si es una referencia constante):

```cpp
std::thread t(update_data_for_widget, w, std::ref(data));
```

Ahora el hilo recibirá una referencia real a `data`, y la función podrá modificarla correctamente.

### Paso de objetos no copiables (uso de `std::move`)

Existen tipos que no pueden copiarse, como `std::unique_ptr`, pero que sí pueden **moverse**. En estos casos, es necesario usar `std::move` para transferir la propiedad del objeto al hilo:

```cpp
void process_big_object(std::unique_ptr<big_object> ptr);

int main() {
    std::unique_ptr<big_object> p(new big_object);
    p->prepare_data(42);

    std::thread t(process_big_object, std::move(p));
    t.join();
}
```

Aquí, la propiedad del puntero se transfiere al hilo, y el objeto `p` en el hilo principal queda vacío. Este enfoque es muy útil cuando se desea pasar recursos dinámicos de forma segura y eficiente.

### Funciones miembro y lambdas

`std::thread` también puede ejecutar funciones miembro de una clase. En ese caso, el primer argumento debe ser el **puntero al objeto** sobre el cual se invocará el método:

```cpp
class X {
public:
    void do_lengthy_work() {
        std::cout << "Ejecutando trabajo largo...\n";
    }
};

int main() {
    X x;
    std::thread t(&X::do_lengthy_work, &x);
    t.join();
}
```

Esto ejecutará [`x.do`](http://x.do)`_lengthy_work()` en el nuevo hilo.

De manera análoga, es posible usar **lambdas** para encapsular tanto la función como los argumentos:

```cpp
int main() {
    int value = 10;
    std::thread t([value]() {
        std::cout << "Valor: " << value << '\n';
    });
    t.join();
}
```

Las lambdas capturan los valores según sus reglas (`[value]`, `[&value]`, `[=]`, `[&]`), lo que permite controlar explícitamente si se copian o se referencian los datos.

### Consideraciones sobre el tiempo de vida

Es fundamental garantizar que los objetos referenciados por el hilo **permanezcan válidos** mientras el hilo los use.  
Esto implica:

*   Si pasas referencias (con `std::ref`), asegúrate de que el objeto exista al menos hasta que el hilo finalice.
    
*   Si pasas punteros, evita apuntar a variables automáticas que puedan salir de alcance.
    
*   Si usas `detach()`, ten cuidado: el hilo puede seguir ejecutándose después de que el contexto local haya terminado.
    

En general, es más seguro pasar copias o usar `std::shared_ptr` si el objeto necesita sobrevivir más allá del alcance del hilo que lo creó.

## Transferir la propiedad de un hilo

Cada objeto `std::thread` **posee** un hilo de ejecución: es el responsable de gestionar su ciclo de vida, ya sea esperando su finalización mediante `join()` o liberándolo con `detach()`.

Esta relación exclusiva implica que **solo un objeto** `std::thread` puede poseer un hilo determinado a la vez.

A diferencia de otros tipos copiables, los hilos **no pueden duplicarse**, porque esto implicaría que dos objetos intentaran controlar el mismo recurso del sistema operativo. Sin embargo, sí pueden **transferirse** entre objetos mediante *move semantics* —el mismo mecanismo que utilizan clases como `std::unique_ptr` para transferir propiedad de recursos únicos.

### Propiedad y movimiento de hilos

El siguiente ejemplo muestra cómo puede moverse la propiedad de un hilo entre distintos objetos `std::thread`:

```cpp
void some_function();
void some_other_function();

int main() {
    std::thread t1(some_function);        // t1 posee el hilo
    std::thread t2 = std::move(t1);       // t2 toma la propiedad
    t1 = std::thread(some_other_function); // t1 crea y posee un nuevo hilo
    std::thread t3;                       // hilo vacío
    t3 = std::move(t2);                   // t3 toma el hilo original
    t1 = std::move(t3);                   // ¡Error! std::terminate()
}
```

El flujo de propiedad es el siguiente:

1.  `t1` crea un hilo que ejecuta `some_function`.
    
2.  `t2 = std::move(t1)` transfiere la propiedad del hilo de `t1` a `t2`.  
    Después de esto, `t1` queda sin hilo asociado.
    
3.  `t1 = std::thread(some_other_function)` inicia un nuevo hilo y se convierte en su dueño.
    
4.  `t3` toma el hilo de `t2` con `std::move(t2)`.
    
5.  Finalmente, la reasignación `t1 = std::move(t3)` provoca la terminación del programa, ya que `t1` aún era dueño de un hilo no finalizado.
    

Este último punto es importante: **asignar un nuevo hilo a un objeto que ya posee uno activo invoca** `std::terminate()`.  
La norma impone esto para mantener la consistencia con el destructor de `std::thread`, que también requiere que el hilo se haya sincronizado o desacoplado antes de destruir el objeto.

### Transferencia de hilos entre funciones

El soporte de movimiento en `std::thread` permite **devolver o recibir hilos por valor** en funciones, algo muy útil para diseñar interfaces limpias y seguras.

```cpp
std::thread create_thread() {
    return std::thread([] {
        std::cout << "Ejecutando hilo...\n";
    });
}

void consume_thread(std::thread t) {
    if (t.joinable()) t.join();
}

int main() {
    std::thread t = create_thread();   // El hilo se transfiere por retorno
    consume_thread(std::move(t));      // Se pasa la propiedad a la función
}
```

En este ejemplo:

*   `create_thread()` devuelve un `std::thread` movido, que transfiere su propiedad al llamador.
    
*   `consume_thread()` acepta el hilo **por valor**, y puede unirse a él sin preocuparse de interferir con otros dueños.
    

La transferencia explícita mediante `std::move()` evita copias ilegales y garantiza que solo exista un dueño válido del hilo en cada momento.

### Clases auxiliares: `scoped_thread` y `joining_thread`

A menudo es conveniente encapsular la gestión del hilo dentro de un objeto que **asegure la sincronización automática al salir del ámbito**. Una forma sencilla de hacerlo es con una clase llamada `scoped_thread`, que toma la propiedad de un hilo en su constructor y lo une en su destructor:

```cpp
class scoped_thread {
    std::thread t;
public:
    explicit scoped_thread(std::thread t_) : t(std::move(t_)) {
        if (!t.joinable())
            throw std::logic_error("No thread");
    }
    ~scoped_thread() {
        t.join();
    }

    scoped_thread(const scoped_thread&) = delete;
    scoped_thread& operator=(const scoped_thread&) = delete;
};
```

Su uso es simple y seguro:

```cpp
void task();

int main() {
    scoped_thread worker(std::thread(task));  // El hilo se une automáticamente al salir
}
```

Gracias a este patrón, se evita olvidar la llamada a `join()`, reduciendo el riesgo de errores y cierres abruptos del programa.

Una variante más flexible es la clase `joining_thread`, que se comporta como un `std::thread` estándar pero **se une automáticamente en su destructor**. Esto permite usarla de forma más natural en estructuras dinámicas o funciones que retornan hilos

```cpp
class joining_thread {
    std::thread t;
public:
    joining_thread() noexcept = default;

    template <typename Callable, typename... Args>
    explicit joining_thread(Callable&& f, Args&&... args)
        : t(std::forward<Callable>(f), std::forward<Args>(args)...) {}

    joining_thread(joining_thread&& other) noexcept
        : t(std::move(other.t)) {}

    joining_thread& operator=(joining_thread&& other) noexcept {
        if (joinable()) join();
        t = std::move(other.t);
        return *this;
    }

    ~joining_thread() noexcept {
        if (joinable()) join();
    }

    bool joinable() const noexcept { return t.joinable(); }
    void join() { t.join(); }
    void detach() { t.detach(); }
};
```

De esta forma, `joining_thread` combina la seguridad de `scoped_thread` con la flexibilidad de `std::thread`.

### Contenedores de hilos

El soporte de movimiento también permite almacenar hilos en **contenedores dinámicos** como `std::vector`.

Esto resulta útil para lanzar múltiples tareas y luego sincronizarlas en grupo:

```cpp
void do_work(unsigned id) {
    std::cout << "Trabajando en hilo " << id << "\n";
}

int main() {
    std::vector<std::thread> threads;

    for (unsigned i = 0; i < 8; ++i)
        threads.emplace_back(do_work, i);

    for (auto& t : threads)
        if (t.joinable()) t.join();
}
```

Cada hilo se crea y se almacena dentro del vector mediante **movimiento implícito**, y luego todos son sincronizados al final del programa.  
Esta técnica permite administrar un número variable de hilos sin declarar múltiples variables, facilitando la creación de *thread pools* o sistemas de tareas paralelas.

* * *

## **Elegir el número de hilos en tiempo de ejecución**

Determinar cuántos hilos crear en un programa concurrente es una de las decisiones más importantes al diseñar una aplicación paralela eficiente. Crear demasiados hilos puede saturar el sistema operativo con cambios de contexto innecesarios; crear muy pocos desaprovecha el potencial de paralelismo del hardware.

Por ello, el número de hilos óptimo debe elegirse **en tiempo de ejecución**, tomando en cuenta los recursos físicos disponibles y el tipo de carga de trabajo.

#### Consultar el hardware: `std::thread::hardware_concurrency()`

La biblioteca estándar de C++ proporciona una función que sirve como guía inicial para decidir cuántos hilos pueden ejecutarse realmente de forma concurrente:

```cpp
unsigned int n = std::thread::hardware_concurrency();
```

Esta función devuelve el número de *hardware threads* (generalmente, el número de núcleos o núcleos lógicos) disponibles para el programa.  
Por ejemplo, en un CPU con cuatro núcleos físicos y *hyper-threading*, el valor devuelto podría ser `8`.

Sin embargo, es importante entender que este valor es solo **una sugerencia**.  
La implementación puede devolver `0` si la información no está disponible, por lo que es buena práctica definir un valor por defecto:

```cpp
unsigned int num_threads = std::thread::hardware_concurrency();
if (num_threads == 0)
    num_threads = 2;  // valor por defecto razonable
```

Este número sirve como punto de partida para asignar trabajo entre hilos, pero no siempre representa la cantidad ideal: en tareas muy ligeras o con fuerte interacción entre hilos, crear un hilo por núcleo puede no ser lo más eficiente.

### Estrategia básica: dividir la carga de trabajo

Una forma sencilla de aplicar este principio es dividir un conjunto de datos entre varios hilos.

El ejemplo siguiente implementa una versión paralela del algoritmo `std::accumulate`, que suma los elementos de un rango, dividiéndolos entre varios hilos según la cantidad de núcleos disponibles.

**accumulate\_CD\_04**

```cpp
template <typename Iterator, typename T>
struct accumulate_block {
    void operator()(Iterator first, Iterator last, T& result) {
        result = std::accumulate(first, last, result);
    }
};

template <typename Iterator, typename T>
T parallel_accumulate(Iterator first, Iterator last, T init) {
    unsigned long const length = std::distance(first, last);
    if (!length)
        return init;

    unsigned long const min_per_thread = 25;
    unsigned long const max_threads =
        (length + min_per_thread - 1) / min_per_thread;

    unsigned long const hardware_threads =
        std::thread::hardware_concurrency();

    unsigned long const num_threads =
        std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads);

    unsigned long const block_size = length / num_threads;

    std::vector<T> results(num_threads);
    std::vector<std::thread> threads(num_threads - 1);

    Iterator block_start = first;
    for (unsigned long i = 0; i < (num_threads - 1); ++i) {
        Iterator block_end = block_start;
        std::advance(block_end, block_size);
        threads[i] = std::thread(
            accumulate_block<Iterator, T>(),
            block_start, block_end, std::ref(results[i])
        );
        block_start = block_end;
    }

    accumulate_block<Iterator, T>()(
        block_start, last, results[num_threads - 1]
    );

    for (auto& t : threads)
        t.join();

    return std::accumulate(results.begin(), results.end(), init);
}
```

* * *

#### Análisis del algoritmo

1.  **Verificación del tamaño de entrada**  
    Si el rango está vacío, simplemente se devuelve el valor inicial `init`.  
    Esto evita lanzar hilos innecesarios cuando no hay trabajo que hacer.
    
2.  **Límite mínimo por hilo**  
    Se define un número mínimo de elementos por hilo (`min_per_thread`), con el fin de evitar la sobrecarga que supondría crear muchos hilos para tareas pequeñas.
    
3.  **Cálculo del número máximo de hilos**  
    A partir del tamaño del rango y del mínimo por hilo, se obtiene el número máximo de hilos que tendría sentido crear.
    
4.  **Número real de hilos a usar**  
    Se elige el menor valor entre el número máximo calculado y el número de hilos de hardware disponibles. Además, si la consulta al hardware falla, se usa un valor por defecto (en este caso, 2).
    
5.  **División del trabajo**  
    El tamaño del bloque asignado a cada hilo se calcula dividiendo la longitud total entre el número de hilos (`block_size`).
    
6.  **Creación y ejecución de hilos**  
    Se lanzan `num_threads - 1` hilos, cada uno procesando una parte del rango.  
    El hilo principal procesa el último bloque para evitar crear un hilo adicional.
    
7.  **Sincronización final**  
    Todos los hilos creados se sincronizan mediante `join()`, y luego se combinan los resultados parciales con un último `std::accumulate`.
    

* * *

### Consideraciones sobre rendimiento

El objetivo de este enfoque es **maximizar la utilización del hardware** evitando el fenómeno de *oversubscription*, que ocurre cuando hay más hilos activos que núcleos disponibles. El exceso de hilos genera cambios de contexto constantes, lo que degrada el rendimiento en lugar de mejorarlo.

Algunos puntos clave:

*   **Sobrecarga de creación:** lanzar un hilo tiene un costo no trivial; conviene hacerlo solo cuando el trabajo lo justifique.
    
*   **Equilibrio de carga:** si los hilos procesan bloques de distinto tamaño o complejidad, algunos núcleos quedarán inactivos antes que otros.
    
*   **Afinidad de CPU:** en casos de alta demanda puede ser beneficioso fijar ciertos hilos a núcleos específicos, aunque esto se gestiona a nivel del sistema operativo.
    
*   **Reutilización de hilos:** en tareas repetitivas conviene emplear un *thread pool*, que mantiene un conjunto fijo de hilos reutilizables en lugar de crearlos y destruirlos continuamente.
    

![](https://cdn.hashnode.com/uploads/covers/68426eab6c41660cfe4c8c42/be0ef6e2-0faf-422a-a618-bbd1fd0dac0d.webp align="center")

* * *

#### Ejemplo práctico: elegir dinámicamente según carga

Una mejora práctica consiste en ajustar el número de hilos **según la carga real** y no solo por hardware. Por ejemplo:

```cpp
unsigned int hardware = std::thread::hardware_concurrency();
unsigned int num_threads = std::min(hardware != 0 ? hardware : 2,
                                    total_tasks / min_work_per_thread);
num_threads = std::max(1u, num_threads);  // garantizar al menos un hilo
```

Esta estrategia evita crear más hilos de los necesarios cuando la tarea es pequeña, pero aprovecha al máximo los núcleos disponibles cuando hay suficiente trabajo.

* * *

## Identificar hilos

Cuando se trabaja con múltiples hilos, puede ser necesario distinguir cuál de ellos está ejecutando una determinada parte del código. Por ejemplo, podríamos querer registrar qué hilo está procesando una tarea, asignar recursos según el hilo, o simplemente generar trazas para depurar comportamientos concurrentes.  
La biblioteca estándar de C++ proporciona un mecanismo seguro y eficiente para realizar esta identificación mediante la clase `std::thread::id`.

* * *

### Obtener el identificador de un hilo

Existen dos formas principales de obtener un identificador de tipo `std::thread::id`:

1.  **Desde un objeto** `std::thread`
    
    ```cpp
    std::thread t(f);
    std::thread::id id = t.get_id();
    ```
    

El método `get_id()` devuelve el identificador del hilo asociado al objeto.

Si el objeto `std::thread` **no está asociado** a ningún hilo de ejecución (por ejemplo, porque fue creado sin función o ya se le hizo `join()` o `detach()`), la llamada devuelve un identificador por defecto, que representa *“ningún hilo”*.

2.  **Desde el hilo actual**
    
    ```cpp
    std::thread::id id = std::this_thread::get_id();
    ```
    
    Esta función devuelve el identificador del hilo que la invoca. Es especialmente útil dentro de funciones que pueden ser ejecutadas por distintos hilos, ya que permite saber cuál de ellos está ejecutando la llamada.
    

#### Propiedades del identificador

Los objetos de tipo `std::thread::id` son **copiables y comparables**. Esto permite usarlos de forma natural para verificar si dos hilos son el mismo:

```cpp
if (t1.get_id() == t2.get_id())
    std::cout << "Ambos objetos representan el mismo hilo\n";
```

Si dos identificadores son iguales, representan el mismo hilo o ambos son “ningún hilo”. Además, la clase proporciona un **orden total**: pueden compararse con `<`, `>`, etc., lo que permite usarlos como **claves en contenedores asociativos**, tanto ordenados (`std::map`) como no ordenados (`std::unordered_map`), gracias a que existe una especialización de `std::hash<std::thread::id>`.

Ejemplo:

```cpp
std::unordered_map<std::thread::id, std::string> thread_names;
thread_names[std::this_thread::get_id()] = "Hilo principal";
```

### Uso en registro y depuración

El identificador de hilo resulta especialmente útil para generar trazas de ejecución.

Por ejemplo, en un sistema concurrente de procesamiento de tareas, podríamos imprimir qué hilo está procesando cada bloque de datos:

```cpp
void process_task(int task_id) {
    std::cout << "Hilo " << std::this_thread::get_id()
              << " procesando tarea " << task_id << '\n';
}
```

Cada ejecución imprimirá un valor distinto de `std::thread::id`, lo que permite identificar fácilmente qué hilo realizó cada operación. El formato exacto del identificador depende de la implementación, pero la norma garantiza que hilos diferentes producirán salidas distintas\*\*, y que \*\*hilos iguales producirán la misma salida.

#### Ejemplo: distinguir el hilo maestro de los trabajadores

Supongamos que el hilo principal lanza varios hilos para realizar trabajo paralelo, pero necesita ejecutar una tarea especial que solo él debe realizar. En ese caso, puede almacenar su propio identificador antes de lanzar los hilos y luego compararlo dentro del código compartido:

```cpp
std::thread::id master_thread;

void some_core_part_of_algorithm() {
    if (std::this_thread::get_id() == master_thread) {
        do_master_thread_work();   // tarea exclusiva del hilo maestro
    }
    do_common_work();              // tarea común a todos los hilos
}

int main() {
    master_thread = std::this_thread::get_id();
    std::thread worker1(some_core_part_of_algorithm);
    std::thread worker2(some_core_part_of_algorithm);

    some_core_part_of_algorithm(); // ejecuta el maestro

    worker1.join();
    worker2.join();
}
```

Aquí, todos los hilos ejecutan la misma función, pero solo el hilo maestro realiza la sección especial al comparar su `std::thread::id` con el almacenado.

* * *

### Asociar datos a hilos mediante identificadores

En ocasiones, es útil mantener información específica de cada hilo, como estadísticas o configuraciones locales.  
Si no se desea usar *thread-local storage*, puede construirse un contenedor donde la clave sea el identificador del hilo:

```cpp
std::map<std::thread::id, ThreadStats> stats_map;

void log_event(std::string event) {
    stats_map[std::this_thread::get_id()].events.push_back(event);
}
```

Este enfoque resulta práctico cuando se requiere que un hilo externo (por ejemplo, un controlador) acceda a información de otros hilos mediante sus identificadores.

### Consideraciones sobre la reutilización de IDs

Aunque los identificadores son **únicos durante la vida activa de un hilo**, los sistemas operativos **pueden reutilizarlos** una vez que un hilo termina y su recurso ha sido liberado. Esto significa que un `std::thread::id` antiguo no debe conservarse para identificar un hilo después de que este haya finalizado, ya que podría ser asignado a otro hilo nuevo.

## Conclusion

La gestión de argumentos, el control sobre la propiedad de los datos y la organización de varios hilos sirven como base práctica para construir programas concurrentes más claros y manejables. Estos mecanismos ayudan a distribuir el trabajo de forma ordenada, a mantener un seguimiento preciso de cada hilo y a evitar errores típicos relacionados con el tiempo de vida de los datos. A partir de aquí, el siguiente paso será es explorar cómo varios hilos pueden acceder y manipular la misma información sin generar inconsistencias. Ese será el punto de partida de la próxima sección: compartir datos entre hilos.

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