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
| Статус | Что означает простыми словами | Что обычно делать |
|---|---|---|
|
результат готов, можно get() | забирать результат |
|
за это время не успело | продолжать ждать, показать прогресс, сделать другое дело |
|
задача отложена и выполнится при 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): процессор будет крутиться как белка в колесе, а пользы ноль. Даже если вам нужно «проверять часто», ставьте разумные интервалы (например, 20ms–100ms) или проектируйте ожидание по-другому.
Ошибка №4: игнорировать статус deferred и надеяться на параллельность.
Если задача создана как отложенная, то wait_for() может вернуть deferred, и никакой «фоновой работы» до get() не будет. В результате индикатор прогресса может выглядеть смешно: вы печатаете точки, а работа ещё даже не начиналась.
Ошибка №5: захватывать в асинхронную задачу ссылки на локальные переменные и потом удивляться странному поведению.
Хотя это больше тема запуска задачи, в future-коде это проявляется особенно неприятно: вы ждёте get(), а внутри задачи обращение идёт к данным, которые уже «умерли». Если данные нужны задаче — захватывайте по значению или обеспечивайте время жизни объектов дольше, чем живёт асинхронная работа.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ