Vai al contenuto

std::async, std::future e std::promise

Usa astrazioni di concorrenza di più alto livello: avvia task asincroni con std::async, recupera i risultati con std::future e imposta manualmente valori o eccezioni con std::promise.

Concorrenza basata sui task

Mentre std::thread ti dà il controllo a basso livello, la concorrenza basata sui task con std::async e std::future è spesso più semplice e meno soggetta a errori. Invece di gestire a mano i thread e lo stato condiviso, esprimi il lavoro come task che producono risultati. È il runtime a decidere come schedularli.

Uno std::future rappresenta un valore di tipo T che sarà disponibile in futuro. Puoi chiamare get() per bloccarti finché il risultato non è pronto. Se il task ha lanciato un'eccezione, get() la rilancia — offrendoti la propagazione automatica delle eccezioni tra thread.

std::async — avviare task asincroni

std::async avvia un callable e restituisce uno std::future che contiene il risultato. Puoi specificare una policy di lancio: std::launch::async impone la creazione di un nuovo thread, mentre std::launch::deferred rimanda l'esecuzione al momento in cui viene chiamata 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;
}

Policy di lancio: async vs deferred

std::launch::async — La funzione viene eseguita su un nuovo thread (o su uno preso da un thread pool: dipende dall'implementazione). Il lavoro inizia immediatamente.

std::launch::deferred — La funzione non viene eseguita finché non chiami get() o wait() sul future. Viene poi eseguita nel thread chiamante (valutazione pigra). Utile per calcoli di cui potresti non avere bisogno.

Predefinita (nessuna policy specificata) — Decide l'implementazione. Può essere async, deferred o entrambe (cioè sceglie a runtime). Se ti serve parallelismo garantito, specifica sempre std::launch::async.

std::promise — impostare il risultato manualmente

std::promise è l'estremità di scrittura di un canale future-promise. Crei una promise, ne estrai il future e poi chiami set_value() oppure set_exception() da qualsiasi thread.

promise_demo.cpp
#include <iostream>
#include <thread>
#include <future>
#include <stdexcept>

void compute(std::promise<int> prom, int x) {
    try {
        if (x < 0) {
            throw std::invalid_argument("negative input");
        }
        int result = x * x;
        prom.set_value(result);  // fulfil the promise
    } catch (...) {
        prom.set_exception(std::current_exception());  // propagate error
    }
}

int main() {
    std::promise<int> prom;
    std::future<int> fut = prom.get_future();  // get the read end

    std::thread t(compute, std::move(prom), 7);

    try {
        int result = fut.get();  // blocks until value or exception
        std::cout << "Result: " << result << "\n";  // prints 49
    } catch (const std::exception& e) {
        std::cout << "Error: " << e.what() << "\n";
    }

    t.join();
    return 0;
}

std::shared_future — più consumatori

Su uno std::future puoi chiamare get() una sola volta (sposta fuori il valore). std::shared_future permette a più thread di attendere e leggere lo stesso risultato.

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;
}
Insidia

Uno std::future restituito da std::async ha una proprietà particolare: il suo distruttore si blocca finché il task non è completato (come un join implicito). Questo significa che, se scarti il future restituito, la chiamata "asincrona" diventa di fatto sincrona:

// SORPRESA — questo si blocca! Il future temporaneo viene distrutto
// al punto e virgola, e la distruzione attende la fine del task.
std::async(std::launch::async, long_computation);

// CORRETTO — conserva il future per lasciare che il lavoro proceda in parallelo
auto fut = std::async(std::launch::async, long_computation);
// ... fai altro lavoro ...
fut.get();  // ora attendi il risultato

Questa è la trappola più comune di std::async. Conserva sempre lo std::future restituito.

std::packaged_task

std::packaged_task incapsula un callable e fornisce uno std::future per il suo risultato. A differenza di std::async, non esegue automaticamente il task — lo invochi tu manualmente (tipicamente spostandolo in un thread o in una coda di task). Questo lo rende ideale per costruire thread pool personalizzati:

1. Incapsula il lavoro in uno std::packaged_task.
2. Estrai lo std::future e consegnalo al chiamante.
3. Sposta il task in una coda di lavoro.
4. I thread worker prelevano i task dalla coda e li invocano.

Buona pratica

Usa std::async quando vuoi ottenere un risultato da del lavoro concorrente e non ti serve un controllo fine sui thread. Propaga automaticamente le eccezioni e gestisce il tempo di vita dei thread. Usa std::thread (o std::jthread) quando ti servono worker persistenti in background, uno scheduling personalizzato, oppure quando stai costruendo infrastruttura come un thread pool. Per il lavoro fire-and-forget, tieni sempre in vita lo std::future restituito, così eviti blocchi accidentali.

Concetti chiave
  • std::async avvia un task e restituisce uno std::future — un'alternativa di alto livello alla gestione manuale dei thread
  • std::launch::async impone un nuovo thread; std::launch::deferred rimanda l'esecuzione alla chiamata di get()
  • std::promise è l'estremità di scrittura di un canale future — usala per impostare manualmente valori o eccezioni
  • std::shared_future permette a più thread di leggere lo stesso risultato
  • Lo std::future restituito da std::async si blocca nel distruttore — conservalo sempre in una variabile
  • std::packaged_task incapsula un callable insieme a un future: ideale per thread pool personalizzati

Quiz — Metti alla prova le tue conoscenze

(15 XP)

1. Cosa succede se scarti lo `std::future` restituito da `std::async(std::launch::async, fn)`?

2. Qual è la differenza tra `std::future` e `std::shared_future`?

3. In che modo `std::async` propaga al chiamante le eccezioni lanciate dal task?