Перейти к содержимому

std::async, std::future и std::promise

Используйте абстракции параллелизма более высокого уровня: запускайте асинхронные задачи со std::async, извлекайте результаты со std::future и вручную устанавливайте значения или исключения со std::promise.

Конкурентность на основе задач

В то время как std::thread дает вам низкоуровневое управление, параллель на основе задач с std::async и std::future часто проще и менее подвержена ошибкам. Вместо того, чтобы вручную управлять потоками и общим состоянием, вы выражаете работу как задачи, которые дают результаты. Время выполнения решает, как их запланировать.

std::future представляет собой значение типа T, которое будет доступно в будущем . Вы можете позвонить в get(), чтобы заблокировать, пока результат не будет готов. Если в задание было добавлено исключение, get() перебрасывает его, обеспечивая автоматическое распространение исключения по потокам.

std::async — запуск асинхронных задач

std::async запускает Callable и возвращает std::future, удерживая результат. Вы можете указать политику запуска : std::launch::async запускает новый поток, в то время как std::launch::deferred задерживает выполнение до тех пор, пока не будет назван 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;
}

Запуск политики: Async vs отложен

std::launch::async Функция выполняется на новом потоке (или из пула потоков, определяемого реализацией). Работа начинается немедленно.

std::launch::deferred Функция ** не выполняется до тех пор, пока get() или wait() не будет вызван в будущее. Он работает в потоке вызовов (ленивая оценка). Полезно для вычислений, которые вам могут не понадобиться.

Дефолт (никакая политика не указана) — Реализация выбирает. Это может быть async, deferred или оба (это означает, что он решает во время выполнения). Если вам нужен гарантированный параллелизм, всегда укажите std::launch::async.

std::promise — ручная настройка результата

std::promise - это write конец канала будущего. Вы создаете обещание, извлекаете его будущее, а затем либо set_value(), либо set_exception() из любого потока.

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 — многопользовательская версия

std::future может быть только get()-ed * * * (он перемещает значение). std::shared_future позволяет множеству потоков ждать и читать один и тот же результат.

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;
}
Подводный камень

std::future, возвращенный std::async, обладает особым свойством: его разрушитель блокирует до завершения задачи (как неявный join). Это означает, что если вы отбросите возвращенное будущее, вызов «асинк» станет фактически синхронным:

// SURPRISE — this blocks! The temporary future is destroyed
// at the semicolon, which waits for the task to finish.
std::async(std::launch::async, long_computation);

// CORRECT — store the future to let work proceed in parallel
auto fut = std::async(std::launch::async, long_computation);
// ... do other work ...
fut.get();  // now wait for the result

Это самая распространенная ошибка с std::async. Всегда захватывайте возвращенный std::future.

Разработчик:packaged task

std::packaged_task оборачивается вызывающим и предоставляет std::future для своего результата. В отличие от std::async, он автоматически запускает задачу — вы вызываете ее вручную (обычно путем перемещения в поток или очередь задач). Это делает его идеальным для строительства обычные резьбовые бассейны :

1. Обернуть работу в std::packaged_task.
2.Извлеките std::future и отдайте его абоненту.
3. Переместить задачу в рабочую очередь.
4. Рабочие нити вытягивают задачи из очереди и вызывают их.

Лучшая практика

Используйте std::async, когда вы хотите получить результат от одновременной работы и не нуждаетесь в тонкозернистом управлении потоками. Он автоматически распространяет исключения и управляет временем жизни потоков. Используйте std::thread (или std::jthread), когда вам нужны постоянные фоновые работники, индивидуальное планирование или когда вы строите инфраструктуру, такую как потоковые пулы. Для работы «пожар и забвение» всегда держите возвращенный std::future живым, чтобы избежать случайного блокирования.

Ключевые выводы
  • std::async запускает задачу и возвращает std::future — высокоуровневую альтернативу ручному управлению потоками
  • std::launch::async создает новую нить; std::launch::deferred задерживается до тех пор, пока не будет назван get()
  • std::promise - это конец записи будущего канала - используйте его для ручной настройки значения / исключения.
  • std::shared_future позволяет нескольким потокам считывать один и тот же результат
  • std::future из блоков std::async в своем деструкторе — всегда храните его в переменной
  • std::packaged_task оборачивается вызывающим будущим, идеальным для пользовательских потоковых пулов

Тест — проверьте свои знания

(15 XP)

1. Что произойдет, если вы откажетесь от `std::future`, возвращенного `std::async(std::launch::async, fn)`?

2. В чем разница между `std::future` и `std::shared_future`?

3. Как `std::async` распространяет исключения из задачи на абонента?