Что происходит с task-local значениями при создании дочерних задач через TaskGroup?
Дочерняя задача, добавленная в TaskGroup, наследует текущие task-local значения родительской задачи на момент своего создания. Это копирование контекста, а не совместное изменяемое хранилище: переопределение значения в дочерней задаче не изменяет его у родителя или соседних задач.
Асинхронный код не может надёжно опираться на локальные переменные потока: после await выполнение может продолжиться на другом потоке. Task-local значения решают задачу передачи контекста выполнения — например, идентификатора запроса, настроек трассировки или локального режима — через цепочку асинхронных вызовов без явной передачи параметров в каждую функцию.
В структурированной конкурентности важно, чтобы такой контекст предсказуемо распространялся на дочерние задачи. Поэтому TaskGroup наследует task-local окружение родителя, сохраняя при этом независимость самих задач.
Предположим, обработчик запроса записал идентификатор корреляции в task-local значение и затем запустил несколько операций через TaskGroup. Если дочерние задачи не увидят этот контекст, логи и трассировка потеряют связь с исходным запросом.
Обратная ошибка тоже опасна: если изменение task-local значения одной дочерней задачей станет видимым другим задачам, появится скрытое общее состояние и зависимость результатов от планирования. Это нарушило бы изоляцию задач.
При вызове addTask дочерняя задача получает значения task-local окружения, действующего в точке добавления. Она видит унаследованное значение независимо от того, на каком исполнителе и потоке фактически будет выполняться.
Task-local значение ведёт себя как контекст с лексической областью действия. Внутри дочерней задачи его можно временно переопределить через withValue; после выхода из этой области восстановится прежнее значение, а исходное значение родительской задачи останется неизменным.
Первая задача напечатает req-42, а вторая внутри временной области — child. После завершения этой области значение req-42 у родителя не изменится.
Это не механизм синхронизации и не замена actor или mutex. Если task-local значение содержит ссылочный объект с изменяемым состоянием, наследоваться будет контекст со ссылкой на тот же объект; безопасность такого объекта нужно обеспечивать отдельно.
Наследование происходит при создании дочерней задачи, поэтому последующее переопределение task-local значения в родителе не меняет уже созданную задачу. В отличие от Task.detached, дочерние задачи TaskGroup сохраняют структурированную связь с родителем и наследуют его контекст выполнения.
Сервис обрабатывает HTTP-запрос и запускает в группе параллельную загрузку профиля, рекомендаций и истории. Идентификатор запроса хранится в task-local значении, чтобы каждая операция автоматически добавляла его в логи.
Можно передавать идентификатор явным параметром во все функции. Это прозрачно и типобезопасно, но увеличивает количество параметров и связывает низкоуровневый код с инфраструктурным контекстом. Можно использовать глобальную переменную, однако она создаёт гонки данных или требует сложной синхронизации и не поддерживает независимые параллельные запросы.
Выбранный вариант — task-local значение на границе обработки запроса и TaskGroup для дочерних операций. Он сохраняет контекст без общего изменяемого состояния, а каждая задача может временно переопределить значение для своего подзапроса. В результате логи корректно коррелируются, а изменения одного дочернего контекста не влияют на остальные.
Изменение task-local значения в дочерней задаче изменяет значение родителя?
Нет. withValue создаёт временное переопределение только для текущей задачи и её потомков. После выхода из области действия прежнее значение восстанавливается; родительская задача не получает обратного изменения.
Увидит ли уже созданная задача новое task-local значение, установленное родителем позже?
Нет. Дочерняя задача получает контекст при создании. Если родитель после addTask переопределит значение, это повлияет только на последующие вызовы и задачи, созданные внутри новой области, но не на уже запущенную задачу.
Достаточно ли task-local значения, чтобы безопасно передать дочерним задачам изменяемый объект?
Нет. Task-local механизм распространяет контекст, но не сериализует доступ к объекту и не устраняет гонки данных. Если несколько задач получили ссылку на один изменяемый экземпляр, его нужно защищать actor, mutex или другой корректной схемой синхронизации; альтернативой является передача независимых неизменяемых снимков.