Рассмотрите фрагмент. Как временный std::future влияет на параллельность двух вызовов std::async?
#include <future>
#include <thread>
void work(int) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
int main() {
std::async(std::launch::async, work, 1);
std::async(std::launch::async, work, 2);
}
Вызовы фактически выполняются последовательно: временный std::future уничтожается в конце полного выражения, а его деструктор ожидает завершения задачи, запущенной с политикой std::launch::async. Поэтому второй вызов начинается только после завершения первого.
Чтобы задачи могли выполняться параллельно, нужно сохранить оба объекта std::future до запуска второй задачи, а затем дождаться их завершения.
std::async и std::future были введены как высокоуровневый механизм запуска вычисления и получения его результата без ручного управления объектом std::thread. Связанное с задачей общее состояние хранит результат, исключение и информацию о завершении.
Ожидание при уничтожении последнего future, связанного с асинхронной задачей, решает проблему времени жизни: программа не должна потерять контроль над выполняющейся задачей, пока та использует свои аргументы или состояние общего состояния.
Вызов std::async возвращает future, но в примере он нигде не сохраняется. Временный объект уничтожается сразу после завершения соответствующего выражения, поэтому его деструктор становится неявной точкой ожидания.
Из-за этого код выглядит асинхронным, но длительность двух вызовов близка к сумме длительностей отдельных задач, а не к длительности наиболее долгой задачи. При большом количестве таких вызовов это может незаметно сериализовать обработку.
Для std::async(std::launch::async, ...) функция должна выполняться асинхронно, однако доступ к её результату контролируется через общее состояние. Если уничтожается последний future, ссылающийся на это состояние, уничтожение ожидает завершения асинхронной функции.
В исходном коде первым таким последним владельцем является временный future. Поэтому порядок действий эквивалентен следующему: запустить первую задачу, дождаться её завершения, уничтожить временный объект, затем запустить вторую задачу.
Правильная организация сохраняет оба future:
Здесь второй вызов запускается до ожидания первого, поэтому задачи могут перекрываться по времени. get() не только ожидает завершения, но и извлекает результат либо повторно выбрасывает исключение, возникшее в асинхронной функции.
Важно отличать временный future от сохранённого. Само наличие политики std::launch::async не отменяет правил времени жизни future. Также не следует полагаться на неявный выбор политики по умолчанию: при std::async(std::launch::async | std::launch::deferred, ...) реализация может выбрать отложенное выполнение.
Сервис обрабатывает два независимых запроса. Разработчик написал два вызова std::async, не сохранив возвращаемые объекты, и обнаружил, что задержка почти удвоилась.
Первый вариант — оставить временные future. Он прост, но полностью сериализует вызовы при явной политике std::launch::async и не даёт управлять моментом ожидания.
Второй вариант — сохранить future в локальных переменных и вызвать get() после запуска всех задач. Он позволяет использовать параллелизм и корректно передаёт исключения, но число одновременно запускаемых задач нужно ограничивать: std::async не является заменой полноценному пулу потоков.
Третий вариант — использовать собственный пул потоков. Он лучше контролирует количество рабочих потоков, очереди и отмену, но требует сложнее управлять синхронизацией, остановкой и ошибками.
Для небольшого фиксированного числа независимых операций выбран второй вариант. Он устранил лишнюю сериализацию без добавления инфраструктуры пула; для большого динамического потока задач был бы предпочтителен пул.
future блокируется?Нет. Ожидание связано с конкретным общим состоянием, созданным std::async с политикой std::launch::async, и уничтожением последнего future, ссылающегося на это состояние. Обычный future, полученный, например, от std::promise, не обязан блокироваться в деструкторе таким же образом.
При std::launch::async | std::launch::deferred реализация выбирает между асинхронным запуском и отложенным выполнением. При выборе deferred функция выполняется в потоке, вызвавшем get() или wait(), поэтому параллельность не гарантируется. Если требуется именно запуск в другом потоке, нужно явно указать std::launch::async.
future не гарантирует ускорение?Оно лишь устраняет немедленное ожидание временного объекта. Реальное ускорение зависит от независимости задач, числа аппаратных ядер, затрат на создание потоков и конкуренции за общие ресурсы. Если задачи используют один мьютекс или выполняются на перегруженной системе, запуск их одновременно может не дать выигрыша и даже ухудшить производительность.