Aller au contenu

std::async, std::future et std::promise

Utilisez des abstractions de concurrence de plus haut niveau : lancez des tâches asynchrones avec std::async, récupérez les résultats avec std::future, et définissez manuellement des valeurs ou des exceptions avec std::promise.

Concurrence orientée tâches

Alors que std::thread vous donne un contrôle de bas niveau, la concurrence orientée tâches avec std::async et std::future est souvent plus simple et moins sujette aux erreurs. Au lieu de gérer manuellement les threads et l'état partagé, vous exprimez le travail sous forme de tâches qui produisent des résultats. C'est l'environnement d'exécution qui décide comment les planifier.

Un std::future représente une valeur de type T qui sera disponible dans le futur. Vous pouvez appeler get() pour bloquer jusqu'à ce que le résultat soit prêt. Si la tâche a levé une exception, get() la relance — vous bénéficiez ainsi d'une propagation automatique des exceptions entre threads.

std::async — lancer des tâches asynchrones

std::async lance un callable et renvoie un std::future contenant le résultat. Vous pouvez préciser une politique de lancement : std::launch::async force la création d'un nouveau thread, tandis que std::launch::deferred retarde l'exécution jusqu'à l'appel de 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;
}

Politiques de lancement : async vs deferred

std::launch::async — La fonction est exécutée sur un nouveau thread (ou sur un thread d'un pool, selon l'implémentation). Le travail commence immédiatement.

std::launch::deferred — La fonction n'est pas exécutée tant que get() ou wait() n'est pas appelé sur le future. Elle s'exécute dans le thread appelant (évaluation paresseuse). Utile pour des calculs dont vous n'aurez peut-être pas besoin.

Par défaut (aucune politique précisée) — L'implémentation choisit. Ce peut être async, deferred, ou les deux (elle décide alors à l'exécution). Si vous avez besoin d'un parallélisme garanti, précisez toujours std::launch::async.

std::promise — définir le résultat manuellement

std::promise est l'extrémité écriture d'un canal future-promise. Vous créez une promise, en extrayez le future, puis appelez set_value() ou set_exception() depuis n'importe quel 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 — plusieurs consommateurs

On ne peut appeler get() sur un std::future qu'une seule fois (la valeur en est déplacée). std::shared_future permet à plusieurs threads d'attendre et de lire le même résultat.

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;
}
Piège

Un std::future renvoyé par std::async a une propriété particulière : son destructeur bloque jusqu'à la fin de la tâche (comme un join implicite). Ainsi, si vous ignorez le future renvoyé, l'appel « asynchrone » devient en pratique synchrone :

// SURPRISE — ceci bloque ! Le future temporaire est détruit
// au point-virgule, ce qui attend la fin de la tâche.
std::async(std::launch::async, long_computation);

// CORRECT — conservez le future pour que le travail s'exécute en parallèle
auto fut = std::async(std::launch::async, long_computation);
// ... faire autre chose ...
fut.get();  // maintenant, attendre le résultat

C'est le piège le plus courant avec std::async. Récupérez toujours le std::future renvoyé.

std::packaged_task

std::packaged_task encapsule un callable et fournit un std::future pour son résultat. Contrairement à std::async, il n'exécute pas automatiquement la tâche — c'est vous qui l'invoquez (généralement en la déplaçant dans un thread ou une file de tâches). Il est donc idéal pour construire des pools de threads personnalisés :

1. Encapsulez le travail dans un std::packaged_task.
2. Extrayez le std::future et remettez-le à l'appelant.
3. Déplacez la tâche dans une file de travail.
4. Les threads de travail récupèrent les tâches dans la file et les invoquent.

Bonne pratique

Utilisez std::async lorsque vous voulez obtenir un résultat d'un travail concurrent sans avoir besoin d'un contrôle fin des threads. Il propage automatiquement les exceptions et gère la durée de vie des threads. Utilisez std::thread (ou std::jthread) lorsque vous avez besoin de threads de travail persistants en arrière-plan, d'une planification personnalisée, ou lorsque vous construisez une infrastructure comme un pool de threads. Pour du travail « lancer et oublier », gardez toujours en vie le std::future renvoyé afin d'éviter un blocage accidentel.

Points clés
  • std::async lance une tâche et renvoie un std::future — une alternative de haut niveau à la gestion manuelle des threads
  • std::launch::async force un nouveau thread ; std::launch::deferred retarde l'exécution jusqu'à l'appel de get()
  • std::promise est l'extrémité écriture d'un canal future — utilisez-le pour définir manuellement une valeur ou une exception
  • std::shared_future permet à plusieurs threads de lire le même résultat
  • Le std::future issu de std::async bloque dans son destructeur — stockez-le toujours dans une variable
  • std::packaged_task associe un callable à un future, idéal pour les pools de threads personnalisés

Quiz — Testez vos connaissances

(15 XP)

1. Que se passe-t-il si vous ignorez le `std::future` renvoyé par `std::async(std::launch::async, fn)` ?

2. Quelle est la différence entre `std::future` et `std::shared_future` ?

3. Comment `std::async` propage-t-il les exceptions de la tâche vers l'appelant ?