Ir al contenido

Futures, Promises y Tareas Asincronas

Usa std::future, std::promise y std::async para comunicacion de valores entre hilos. Comprende las politicas de lanzamiento, shared futures y la propagacion de excepciones a traves de limites de hilo.

El Patron Future-Promise

El patron future-promise proporciona un canal seguro entre hilos para transferir un unico valor. Un std::promise es el extremo del productor — estableces el valor (o excepcion). Un std::future es el extremo del consumidor — esperas y recuperas el resultado.

Este patron desacopla la produccion del consumo: al productor no le importa cuando el consumidor lee, y al consumidor no le importa cuando el productor escribe. La sincronizacion esta incorporada.

std::async simplifica este patron lanzando una tarea (funcion) y devolviendo un future que contendra el resultado cuando la tarea complete. Es la forma mas facil de hacer trabajo en segundo plano en C++.

std::async y Politicas de Lanzamiento

std::async lanza una tarea y devuelve un std::future. La politica de lanzamiento controla si la tarea se ejecuta en un nuevo hilo o diferida hasta que se llame a .get().

async_parallel_sum.cpp
#include <iostream>
#include <future>
#include <chrono>
#include <numeric>
#include <vector>

long long parallel_sum(const std::vector<int>& v, size_t start, size_t end) {
    if (end - start < 1000) {
        return std::accumulate(v.begin() + start, v.begin() + end, 0LL);
    }
    size_t mid = start + (end - start) / 2;

    // Launch left half asynchronously
    auto left = std::async(std::launch::async,
                           parallel_sum, std::cref(v), start, mid);

    // Compute right half in this thread
    long long right_sum = parallel_sum(v, mid, end);

    return left.get() + right_sum;  // .get() blocks until left is done
}

int main() {
    std::vector<int> data(1'000'000, 1);

    auto start = std::chrono::high_resolution_clock::now();
    long long result = parallel_sum(data, 0, data.size());
    auto end = std::chrono::high_resolution_clock::now();

    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
    std::cout << "Sum: " << result << " in " << ms.count() << "ms\n";
    return 0;
}

std::promise — Produccion Manual de Valores

std::promise te da control manual sobre cuando y como se establece un valor para un future. Es util cuando la creacion de hilos esta separada del sitio de produccion de valores.

Propagacion de Excepciones a Traves de Limites de Hilo

Las excepciones no pueden cruzar limites de hilo directamente. Pero std::future proporciona una solucion elegante: si la tarea lanza una excepcion, la excepcion se almacena en el estado compartido del future. Cuando el consumidor llama a .get(), la excepcion se relanza en el hilo del consumidor.

Esto funciona tanto con std::async como con std::promise:

auto f = std::async([] {
    throw std::runtime_error("oops");
    return 42;
});
try {
    int val = f.get();  // relanza la excepcion almacenada
} catch (const std::runtime_error& e) {
    // Manejado en el hilo del llamador
}

Tambien puedes establecer excepciones manualmente con promise.set_exception(std::make_exception_ptr(e)).

std::shared_future — Multiples Consumidores

std::shared_future permite que multiples hilos esperen y lean el mismo resultado. A diferencia de std::future (de movimiento unico), shared_future es copiable y multiples hilos pueden llamar a .get() concurrentemente.

shared_future.cpp
#include <iostream>
#include <future>
#include <thread>
#include <vector>

int main() {
    auto shared = std::async(std::launch::async, []() {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        return 42;
    }).share();  // convert future to shared_future

    // Multiple threads can all read the same result
    std::vector<std::thread> consumers;
    for (int i = 0; i < 5; ++i) {
        consumers.emplace_back([shared, i]() {
            int val = shared.get();  // all 5 threads get 42
            std::cout << "Consumer " << i << " got: " << val << "\n";
        });
    }

    for (auto& t : consumers) t.join();
    return 0;
}
Error común

Un std::future devuelto por std::async tiene una propiedad especial: su destructor se bloquea hasta que la tarea se completa. Esto significa que si descartas el future, la llamada se vuelve efectivamente sincrona:

std::async(std::launch::async, expensive_task);  // Se bloquea aqui!
// El future temporal se destruye inmediatamente, esperando a que la tarea termine

Siempre asigna el resultado de std::async a una variable para mantener el future vivo hasta que realmente quieras esperar.

Este comportamiento de destructor bloqueante es unico de los futures devueltos por std::async — los futures de std::promise o std::packaged_task no se bloquean en la destruccion.

Buena práctica

Elige la herramienta correcta para el trabajo:

std::async — La opcion mas simple. Usalo para tareas de dispara-y-olvida con retorno de resultado. La implementacion gestiona los hilos por ti.

std::promise + std::future — Usalo cuando la creacion de hilos este separada de la produccion de valores, o cuando necesites control manual sobre cuando y como se entrega el resultado.

std::thread crudo — Usalo cuando necesites hilos de larga vida, control del pool de hilos, afinidad de CPU u opciones avanzadas de planificacion que async no proporciona.

Para tareas de solo fuego que no necesitan un resultado, un pool de hilos con una cola de tareas es a menudo mas eficiente que lanzar un nuevo hilo por tarea.

Puntos clave

Use std::async when you want a result from concurrent work and don't need fine-grained thread control. It automatically propagates exceptions and manages thread lifetimes. Use std::thread (or std::jthread) when you need persistent background workers, custom scheduling, or when you're building infrastructure like thread pools. For fire-and-forget work, always keep the returned std::future alive to avoid accidental blocking.

Puntos clave
  • std::async launches a task and returns a std::future — a high-level alternative to manual thread management
  • std::launch::async forces a new thread; std::launch::deferred delays until get() is called
  • std::promise is the write end of a future channel — use it for manual value/exception setting
  • std::shared_future lets multiple threads read the same result
  • The std::future from std::async blocks in its destructor — always store it in a variable
  • std::packaged_task wraps a callable with a future, ideal for custom thread pools

Quiz — Pon a prueba tus conocimientos

(15 XP)

1. Cual es la diferencia entre `std::launch::async` y `std::launch::deferred`?

2. Cual es la diferencia clave entre `std::future` y `std::shared_future`?

3. Como propaga `std::async` excepciones de la tarea al llamador?