JavaRush /Курсы /C++ SELF /std::future:

std::future: get(), wait(), wait_for() и таймауты

C++ SELF
71 уровень , 1 лекция
Открыта

1. Основы std::future: общее состояние, wait() и get()

Когда новичок видит std::future<int>, первая мысль часто такая: «Ага, это как int, только ленивый: сейчас пустой, потом заполнится». Метафора почти правильная, но с важной поправкой: future — это не само значение, а ручка к общему состоянию, где когда-нибудь появится значение (или что-то пойдёт не так).

Удобно представлять так: где-то есть «коробочка результата» (в стандарте это называют shared state), а std::future<T> — это ваш билетик, по которому вы можете либо подождать, либо забрать содержимое. И билетик, как правило, один: один потребитель.

Это «общее состояние результата» — не мифическая сущность, а реальная часть модели future/promise. В частности, важно помнить, что future::get() отпускает связь с этим состоянием: билетик «сгорает» после использования.

wait() и get(): два похожих действия с разным смыслом

На этой теме многие спотыкаются, потому что внешне всё выглядит одинаково: «и там ждём, и тут ждём». Разница в том, что:

  • wait() — это «подожди, пока будет готово»;
  • get() — это «подожди и отдай мне результат».

Звучит похоже, но последствия разные, и в реальном коде это влияет на структуру программы.

wait(): дождаться готовности, но ничего не забирать

wait() нужен, когда вам важно дождаться завершения, но само значение либо не нужно прямо сейчас, либо вы собираетесь забирать его позже (возможно, в другом месте кода). Представьте, что вы поставили пиццу в духовку: wait() — это «стоять у духовки и дождаться таймера», но ещё не доставать пиццу.

Мини-пример: мы запускаем задачу, делаем что-то ещё, и потом просто убеждаемся, что задача закончилась.

#include <future>
#include <chrono>
#include <thread>
#include <iostream>

int main() {
    auto f = std::async(std::launch::async, [] {
        std::this_thread::sleep_for(std::chrono::milliseconds{300});
        return 7;
    });

    f.wait();                 // дождались готовности
    std::cout << "ready\n";   // ready
}

Важный момент: после wait() результат всё ещё внутри «коробочки», и вы можете потом вызвать get() и забрать его (если это future<T>, а не future<void>).

get(): дождаться и забрать результат, «сжечь билет»

get() делает две вещи: ждёт, пока результат будет готов, и возвращает значение (или ничего, если T = void). При этом get() обычно делает future «пустым» — то есть после успешного get() он теряет связь с общим состоянием результата.

Смысл одноразовости — часть контракта future: второй раз забрать результат через тот же std::future нельзя.

Мини-пример с демонстрацией:

#include <future>
#include <iostream>

int main() {
    auto f = std::async(std::launch::async, [] { return 10; });

    int x = f.get();
    std::cout << x << '\n';   // 10

    // Второй get() — логическая ошибка (future уже "потрачен")
}

Мы сознательно не делаем здесь второй get(), чтобы не превращать лекцию в фестиваль исключений и аварийных завершений. Но идею запомните: get() — действие с последствиями.

2. Ожидание с таймаутом: wait_for(), статусы и wait_until()

Почему «ждать вечно» — плохая бизнес-модель

В учебных примерах часто пишут f.get() и довольны. В реальных программах это иногда превращается в «мы зависли навсегда, но зато строго по стандарту». Если вы делаете CLI-утилиту, сервер или даже просто приложение, которое должно оставаться живым, вам нужен способ сказать: «Я подожду чуть-чуть, а если не готово — займусь чем-то ещё».

Для этого у std::future есть семейство ожиданий с таймаутом: wait_for() и wait_until(). Основной фокус — на wait_for().

wait_for(): «подожди не больше N времени» и верни статус

wait_for(duration) — это способ ожидания с ограничением по времени: вы задаёте длительность (например, 100 мс), и future возвращает статус, что произошло за это время.

Ключевая мысль: таймаут — это не «отмена задачи». Это всего лишь наблюдение: «за 100 мс результат не успел стать готовым». Задача, как правило, продолжает выполняться.

Статус описывается через std::future_status. На практике чаще всего встречаются ready и timeout, а ещё есть deferred (отложенный запуск).

Таблица статусов std::future_status

Статус Что означает простыми словами Что обычно делать
std::future_status::ready
результат готов, можно get() забирать результат
std::future_status::timeout
за это время не успело продолжать ждать, показать прогресс, сделать другое дело
std::future_status::deferred
задача отложена и выполнится при wait()/get() понимать, что параллельности нет, и решать — ок ли это

Мини-пример: проверка «готово ли за 100 мс»

#include <future>
#include <chrono>
#include <thread>
#include <iostream>

int main() {
    auto f = std::async(std::launch::async, [] {
        std::this_thread::sleep_for(std::chrono::seconds{1});
        return 123;
    });

    auto st = f.wait_for(std::chrono::milliseconds{100});
    std::cout << (st == std::future_status::timeout) << '\n'; // 1
}

Если вы увидели timeout, это не трагедия. Это просто значит: «ещё не готово».

deferred: когда работа не стартует, пока вы не потребуете результат

Эта часть кажется мелкой, но она объясняет кучу «странностей» в поведении программы. Если задача создана с политикой std::launch::deferred, то она не обязана стартовать сразу. Она может стартовать только тогда, когда вы потребуете результат через wait() или get(). Тогда wait_for() может вернуть статус deferred.

Мы не углубляемся здесь в выбор политики запуска, но умение распознать deferred важно: иначе вы будете искать «почему нет параллельности», а виноват просто контракт запуска.

#include <future>
#include <chrono>
#include <iostream>

int main() {
    auto f = std::async(std::launch::deferred, [] { return 1; });

    auto st = f.wait_for(std::chrono::milliseconds{0});
    std::cout << (st == std::future_status::deferred) << "\n"; // 1

    std::cout << f.get() << "\n"; // 1
}

Здесь нулевой таймаут используется не как цикл ожидания, а как «диагностический вопрос»: «ты вообще запустился или ты ленивый?».

wait_until(): ждать до конкретного дедлайна

Иногда удобнее мыслить не «подожди 200 мс», а «подожди до 12:00:05». Для этого есть wait_until(time_point). Внутри это тоже ожидание, просто срок задаётся абсолютной точкой времени, а не длительностью.

На практике это удобно, когда у вас есть общий дедлайн и вы не хотите «накопить» ошибок ожидания в циклах.

Мини-пример с дедлайном через steady_clock:

#include <future>
#include <chrono>
#include <thread>
#include <iostream>

int main() {
    auto f = std::async(std::launch::async, [] {
        std::this_thread::sleep_for(std::chrono::milliseconds{300});
        return 9;
    });

    auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds{100};
    std::cout << (f.wait_until(deadline) == std::future_status::timeout) << "\n"; // 1
}

В большинстве начинающих задач wait_for() проще. Но знать, что «дедлайн-версия» существует, полезно.

3. Практический пример: «асинхронный расчёт» со спиннером на wait_for()

Соберём мини-программу из маленьких кусочков. Идея простая: запускаем долгий расчёт через std::async, а в main периодически проверяем готовность через wait_for(...) и печатаем точки. Это похоже на то, как консольные утилиты показывают «работаю…».

Долгая функция: симулируем работу

В настоящей жизни тут был бы парсинг гигабайтного файла, запрос в сеть или расчёт модели. В учебной жизни у нас будет «подождать и вернуть число», потому что мы учим future, а не страдания.

#include <chrono>
#include <thread>

int slow_add(int a, int b) {
    std::this_thread::sleep_for(std::chrono::milliseconds{700});
    return a + b;
}

Запуск и ожидание с точками

Главный трюк: wait_for() в цикле. Мы не используем busy-wait с 0ms (это плохая привычка), а даём системе «подышать» разумными интервалами, например 100ms.

#include <future>
#include <chrono>
#include <iostream>

int main() {
    auto f = std::async(std::launch::async, slow_add, 2, 3);

    while (f.wait_for(std::chrono::milliseconds{100}) != std::future_status::ready) {
        std::cout << "."; // ..... (пока считает)
    }

    std::cout << "\n" << f.get() << "\n"; // 5
}

Обратите внимание на структуру: мы вызываем get() только тогда, когда уверены, что статус ready. Это делает поведение кода читаемым: «ждём с прогрессом → когда готово, забираем».

Аккуратная версия с chrono_literals

Чтобы код был дружелюбнее глазам (и меньше похож на бухгалтерию с миллисекундами), можно подключить литералы.

#include <future>
#include <chrono>
#include <iostream>

using namespace std::chrono_literals;

int main() {
    auto f = std::async(std::launch::async, [] {
        std::this_thread::sleep_for(500ms);
        return 42;
    });

    while (f.wait_for(100ms) != std::future_status::ready) std::cout << ".";
    std::cout << "\n" << f.get() << "\n"; // 42
}

Да, тут while в одну строку выглядит слегка дерзко, но для демонстрации допустимо. В реальном коде лучше разворачивать тело цикла, если там появляется логика сложнее одной операции.

4. Типичные ошибки при работе с std::future

Ошибка №1: вызывать get() два раза и ожидать, что будущее бесконечно.
std::future — это не «коробка с копиями результата», а одноразовый доступ к shared state. Если вы хотите использовать значение много раз, заберите его один раз через get() и сохраните в обычную переменную. Иначе вы рано или поздно получите ошибку выполнения и очень грустные глаза.

Ошибка №2: думать, что timeout означает «задача сломалась».
std::future_status::timeout говорит только о том, что за отведённое время результат не стал готов. Это не диагноз и не приговор, а просто «ещё не готово». Типичная здоровая реакция — подождать ещё, показать прогресс пользователю или выполнить альтернативную работу.

Ошибка №3: сделать while (f.wait_for(0ms) != ready) {} и гордо назвать это «таймаутами».
Такой цикл превращается в активное ожидание (busy-wait): процессор будет крутиться как белка в колесе, а пользы ноль. Даже если вам нужно «проверять часто», ставьте разумные интервалы (например, 20ms100ms) или проектируйте ожидание по-другому.

Ошибка №4: игнорировать статус deferred и надеяться на параллельность.
Если задача создана как отложенная, то wait_for() может вернуть deferred, и никакой «фоновой работы» до get() не будет. В результате индикатор прогресса может выглядеть смешно: вы печатаете точки, а работа ещё даже не начиналась.

Ошибка №5: захватывать в асинхронную задачу ссылки на локальные переменные и потом удивляться странному поведению.
Хотя это больше тема запуска задачи, в future-коде это проявляется особенно неприятно: вы ждёте get(), а внутри задачи обращение идёт к данным, которые уже «умерли». Если данные нужны задаче — захватывайте по значению или обеспечивайте время жизни объектов дольше, чем живёт асинхронная работа.

1
Задача
C++ SELF, 71 уровень, 1 лекция
Недоступна
Квадрат по заказу
Квадрат по заказу
1
Задача
C++ SELF, 71 уровень, 1 лекция
Недоступна
Ожидание статуса
Ожидание статуса
1
Задача
C++ SELF, 71 уровень, 1 лекция
Недоступна
Три проверки
Три проверки
1
Задача
C++ SELF, 71 уровень, 1 лекция
Недоступна
Статус двух задач
Статус двух задач
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ