Сравните повторное ожидание одного дескриптора Task с новым запуском асинхронной операции.
Повторное ожидание одного и того же дескриптора Task не запускает работу заново: все ожидающие получают один сохранённый результат или одну сохранённую ошибку. Новый запуск асинхронной операции создаёт другое выполнение и может привести к повторному побочному эффекту.
Модель async/await отделяет описание асинхронной операции от управления её выполнением. Дескриптор Task нужен не только для запуска работы, но и для последующего присоединения к уже выполняющемуся вычислению.
Такой подход решает типичную проблему callback-моделей: несколько потребителей могут ждать один результат без ручного распределения callback-функций и без запуска одинаковой операции несколько раз.
Предположим, несколько экранов одновременно запрашивают один ресурс. Если каждый вызов запускает собственную асинхронную операцию, возникнут дублирование сетевых запросов, лишняя нагрузка и возможные гонки при обновлении общего состояния.
Если же сохранить один дескриптор Task и передать его потребителям, они присоединятся к одному вычислению. Важно отличать это от повторного вызова функции, которая создаёт новый Task: такой вызов начнёт отдельную операцию.
Task представляет конкретный экземпляр выполняемой работы. После завершения его результат сохраняется внутри этого экземпляра, поэтому последующее обращение к value получает тот же результат без повторного выполнения тела задачи.
В примере сообщение о работе печатается один раз, а оба ожидания получают значение 42. Если задача завершается ошибкой, повторное ожидание того же дескриптора снова наблюдает завершение этой задачи с той же ошибкой.
Это не означает, что Task автоматически является полноценным кэшем бизнес-данных. Дескриптор нужно сохранить и сделать доступным потребителям; после его замены новой задачей последующие вызовы могут присоединиться уже к другому вычислению.
Отмена также требует отдельного внимания. Отмена ожидающего потребителя не означает автоматическую отмену общей задачи-производителя, а флаг отмены сам по себе не прерывает произвольный код. Производитель должен сотрудничать с отменой, а владелец дескриптора должен явно определить, когда общую работу следует отменять.
Сервис загрузки изображений получает пять одновременных запросов на один URL. Вариант с запуском отдельного Task для каждого запроса прост, но создаёт пять сетевых операций. Вариант с глобальным словарём готовых данных экономит запросы, однако не решает проблему параллельных промахов: несколько потребителей могут одновременно не найти запись и всё равно начать загрузку.
Практичное решение — хранить в словаре не только готовые изображения, но и дескрипторы текущих задач загрузки. Первый запрос создаёт задачу, остальные получают тот же дескриптор и ожидают его value. После завершения результат можно перенести в кэш, а запись о выполняющейся задаче удалить; это предотвращает бесконечное удержание завершённых задач и позволяет корректно повторить неудачную загрузку.
Нет. Ожидание task.value присоединяется к конкретному экземпляру Task. После завершения его результат или ошибка сохраняются, поэтому последующие ожидания не повторяют тело задачи и не воспроизводят её побочные эффекты.
Нет, автоматически это не гарантируется. Отмена потребителя изменяет состояние именно этой задачи; дескриптор общей задачи не получает от этого обязательную отмену. Если общая работа больше не нужна, приложение должно явно определить политику: отменять её при отсутствии потребителей или продолжать ради кэша.
Да. Его value можно ожидать после завершения, и будет возвращён уже сохранённый результат либо повторно сообщена ошибка. Однако сохранение дескриптора само по себе не превращает результат в бессрочный кэш: оно удерживает состояние задачи, а правила хранения, устаревания и повторной загрузки нужно реализовать отдельно.