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().
#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.
#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.
#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;
}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ésultatC'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.
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.
std::asynclance une tâche et renvoie unstd::future— une alternative de haut niveau à la gestion manuelle des threadsstd::launch::asyncforce un nouveau thread ;std::launch::deferredretarde l'exécution jusqu'à l'appel deget()std::promiseest l'extrémité écriture d'un canal future — utilisez-le pour définir manuellement une valeur ou une exceptionstd::shared_futurepermet à plusieurs threads de lire le même résultat- Le
std::futureissu destd::asyncbloque dans son destructeur — stockez-le toujours dans une variable std::packaged_taskassocie 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 ?