Почему захват несамопередаваемого значения обычно допустим в Task, но запрещается в Task.detached?
Task обычно наследует контекст создания, включая изоляцию текущего актора, поэтому захваченное значение может оставаться внутри защищённого контекста. Task.detached запускается независимо от родительской задачи и её изоляции, поэтому его замыкание должно безопасно передавать захваченные значения между конкурентными контекстами; несамопередаваемые значения такой гарантии не дают.
Structured concurrency и actors появились в Swift, чтобы явно описывать границы конкурентного доступа и уменьшать число гонок данных. Для задач, связанных с текущей операцией, полезно наследовать контекст родителя, а для полностью независимой работы нужен механизм, не зависящий от родительской задачи.
Task.detached решает именно вторую задачу: он не наследует изоляцию актора, task-local значения, приоритет и отмену родительской задачи. Поэтому граница передачи данных в detached-задачу должна быть проверена строже.
Несамопередаваемый ссылочный объект может одновременно использоваться исходным контекстом и detached-задачей. Даже если сейчас задача только читает объект, другой поток или задача могут изменить его состояние, что создаёт гонку данных или нарушение инвариантов.
Наличие await не исправляет проблему: оно лишь позволяет задаче приостановиться. Само по себе ожидание не делает ссылочный объект потокобезопасным и не добавляет ему синхронизацию.
Замыкание обычной Task наследует конкурентный контекст. Например, задача, созданная внутри изолированного метода @MainActor, обычно продолжает выполняться в изоляции главного актора. Доступ к несамопередаваемому объекту в таком замыкании может быть безопасным, если весь доступ остаётся в этой границе.
Task.detached не наследует эту изоляцию. Его замыкание рассматривается как независимое @Sendable-замыкание, поэтому захваченные значения должны быть Sendable или иным образом безопасно передаваться между конкурентными контекстами.
В примере Session — обычный изменяемый ссылочный тип, поэтому его захват в detached-задачу не даёт гарантии безопасности. Вместо передачи объекта извне можно передать безопасный снимок состояния, как показано со значением String.
Другой вариант — передавать не сам объект, а изолированный интерфейс, например обращаться к состоянию через actor. Это сохраняет безопасность, но каждый доступ становится асинхронным и может иметь дополнительную стоимость. Объявление типа как @unchecked Sendable не устраняет гонку автоматически: оно лишь отключает проверку компилятора и переносит ответственность за синхронизацию на разработчика.
Сервис авторизации хранит изменяемую сессию в обычном классе. Разработчик запускает тяжёлую фоновую работу через Task.detached и передаёт туда объект сессии, чтобы периодически читать токен.
Можно захватить сам объект. Это проще, но небезопасно: сессия может измениться одновременно с чтением, а компилятор справедливо отвергнет такую передачу. Можно пометить класс @unchecked Sendable, но это только подавит диагностику и создаст скрытый риск.
Можно передать неизменяемую копию нужных данных — например строковый токен. Такой вариант выбран: detached-задача получает минимальный Sendable-снимок, не зависит от жизненного цикла сессии и не конкурирует с её изменениями. Если же требуется видеть актуальное состояние, сессию следует перестроить вокруг actor, а не передавать обычный класс между задачами.
1. Достаточно ли того, что detached-задача только читает несамопередаваемый объект?
Нет. Проверяется возможность безопасной передачи значения между конкурентными контекстами, а не намерение конкретного разработчика. Другой код всё ещё может изменять объект, а сам объект может иметь внутреннее изменяемое состояние.
2. Сделает ли копирование ссылочного объекта передачу безопасной?
Не обязательно. Поверхностная копия может продолжать ссылаться на те же изменяемые вложенные объекты. Безопасным является снимок, составленный из действительно Sendable-значений, либо объект с корректной внутренней синхронизацией.
3. Что изменится, если вместо Task.detached использовать Task внутри метода актора?
Обычная Task наследует контекст родителя, поэтому её работа может быть изолирована тем же актором. Это упрощает безопасный доступ к состоянию, но задача всё равно остаётся отдельной задачей со своим жизненным циклом; наследование изоляции не означает автоматического ожидания или автоматической отмены во всех сценариях.