Программирование RustКонкурентность и asyncRust-разработчик серверных систем

Чем ожидание нескольких futures внутри одной async задачи отличается от запуска их как отдельных задач с то...

Чем ожидание нескольких futures внутри одной async-задачи отличается от запуска их как отдельных задач с точки зрения параллельного выполнения?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Ожидание нескольких futures внутри одной async-задачи обеспечивает конкурентное продвижение, но само по себе не создаёт отдельных задач или параллельного выполнения на разных потоках. Запуск через runtime создаёт независимые задачи, которые runtime может планировать отдельно и, в многопоточном режиме, выполнять параллельно.

Исторический контекст

Модель futures появилась для представления незавершённых операций без выделения отдельного потока на каждую из них. Это решало проблему большого числа операций ввода-вывода, когда потоки большую часть времени простаивают в ожидании.

Разделение композиции futures и запуска задач позволяет отдельно описывать зависимость операций и решать, какие из них должны иметь независимый жизненный цикл и участвовать в планировании runtime.

Постановка проблемы

Предположим, сервис одновременно ожидает ответ от базы данных и сетевой запрос. Если объединить futures внутри одной задачи, они могут продвигаться по очереди при каждом вызове poll, освобождая поток на время ожидания.

Однако это не означает автоматического использования нескольких ядер. Ошибка в понимании этого различия приводит к ожиданию ускорения CPU-bound работы или к неправильному выбору границ отмены, обработки ошибок и владения ресурсами.

Подробное решение

При композиции futures внутри одной async-задачи родительский future обычно опрашивает дочерние futures. Если один из них не готов, он возвращает Pending; runtime получает возможность выполнять другие задачи. Дочерние операции остаются частью одного общего future и не получают независимых очередей планирования.

Такой подход даёт конкурентность: пока одна операция ждёт ввод-вывод, другая может продвигаться. Но на одном потоке их код выполняется поочерёдно, а параллельность возможна только если разные части действительно исполняются на разных потоках и допускают это моделью runtime.

При запуске отдельных задач runtime регистрирует их независимо. Он может приостанавливать одну задачу, возобновлять другую и, в многопоточном runtime, распределять задачи между рабочими потоками. Поэтому отдельные задачи подходят для независимых фоновых операций, но обычно требуют более строгих ограничений вроде Send и 'static, если задача может перемещаться между потоками или жить дольше вызывающего контекста.

Различается и жизненный цикл. Объединённая композиция обычно завершается вместе с родительской задачей: её отмена прекращает дальнейшее продвижение дочерних futures. Независимая задача может продолжить работу после того, как вызывающая функция перестала её ожидать, если её явно не отменить или не дождаться.

Различается обработка ошибок. В композиции ошибка одного future часто немедленно влияет на общий результат и может прекратить ожидание остальных. У независимых задач ошибки нужно собирать через их дескрипторы, каналы или другой механизм координации.

Ситуация из практики

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

Композиция двух futures уменьшает задержку до времени более медленного запроса и не создаёт лишних независимых задач. Это хороший выбор, если оба запроса нужны только для текущего ответа и должны завершаться или отменяться вместе.

Запуск отдельных задач даёт независимое планирование и позволяет переиспользовать результаты, отправлять их другим потребителям или продолжать работу после завершения текущего обработчика. Минусы — более сложная отмена, сбор ошибок и контроль времени жизни; кроме того, сам факт запуска задачи не делает CPU-bound вычисления безопасными для потока runtime.

Для описанного запроса обычно выбирают композицию futures внутри одной задачи: зависимости простые, общий результат нужен в одном месте, а независимый жизненный цикл не требуется. На многопоточность рассчитывают только при явном вынесении CPU-bound работы в специальный пул или отдельную задачу, поддержанную runtime.

Что кандидаты часто упускают

  1. Вопрос: Гарантирует ли композиция нескольких futures одновременное начало всех операций?

    Ответ: Нет. Futures ленивы: операция начинает продвигаться только при опросе. Композиция может опрашивать несколько дочерних futures в рамках одного цикла, но порядок и частота опроса определяются реализацией комбинатора и runtime. Для сетевых операций это обычно достаточно, чтобы они были зарегистрированы и ожидались конкурентно, но строгой гарантии одновременного старта нет.

  2. Вопрос: Может ли отдельная задача выполняться параллельно с родительской на однопоточном runtime?

    Ответ: Нет, физически одновременно — не может. Runtime переключается между задачами кооперативно: текущая задача должна вернуть управление, обычно достигнув ожидания или завершившись. Поэтому отдельные задачи на одном потоке дают независимое планирование и конкурентность, но не параллельное выполнение.

  3. Вопрос: Почему запуск отдельной задачи не является универсальным способом ускорить вычисление?

    Ответ: Async-задача не создаёт автоматически дополнительный поток и не освобождает поток во время непрерывного CPU-bound кода без точек ожидания. Такая задача может надолго занять рабочий поток runtime и задержать остальные задачи. Для тяжёлых вычислений используют выделенный blocking-пул, специализированный пул CPU-задач или явные потоки, а async-задачи оставляют для координации и ожидания ввода-вывода.