Что произойдёт с изоляцией текущего актора, если внутри его метода создать обычную Task вместо Task.detached?
Обычная Task наследует контекст создания, включая текущую actor isolation, поэтому её замыкание обычно остаётся изолированным тем же актором. Task.detached создаёт независимую задачу без наследования изоляции актора, поэтому доступ к actor-isolated состоянию из неё требует явного взаимодействия с актором.
До появления structured concurrency разработчики часто передавали работу между очередями GCD или создавали фоновые потоки вручную. Это давало низкоуровневый контроль, но усложняло сохранение контекста выполнения и проверку безопасного доступа к общим данным.
Модель async/await, actors и Task сделала контекст выполнения частью проверяемой модели Swift Concurrency. Различие между наследующей и независимой задачей позволяет явно выбрать: продолжить работу в текущей изоляции или полностью отделить её от родительского контекста.
Предположим, метод актора запускает фоновую работу, которая обращается к его изолированному состоянию. Если разработчик ошибочно считает любую Task независимой, он может выбрать Task.detached и попытаться использовать состояние актора напрямую.
Такой код не должен проходить проверку изоляции: detached-задача не получает actor isolation. Если же без необходимости использовать обычную Task, работа сохранит привязку к актору и может конкурировать за его сериализованный исполнитель, что иногда снижает параллелизм.
Task { ... } — это наследующая задача. При создании внутри actor-isolated контекста она наследует actor isolation, приоритет и значения task-local storage. Поэтому тело такой задачи может обращаться к состоянию текущего актора в рамках его изоляции; это не означает, что задача выполняется немедленно или параллельно с другими методами актора.
Task.detached { ... } — независимая задача верхнего уровня. Она не наследует actor isolation, task-local values и контекст приоритета родительской задачи. Захваченные значения должны соответствовать требованиям конкурентной передачи, а обращение к состоянию актора выполняется через его изолированный интерфейс, обычно с await.
Обычная Task не становится автоматически дочерней задачей в смысле structured concurrency: она может продолжить работу после завершения вызывающего метода. Для строгой структурированной связи применяются async let и task groups. Поэтому выбор Task вместо Task.detached решает вопрос наследования контекста, но не заменяет проектирование жизненного цикла задачи и обработки отмены.
Главный компромисс таков: наследующая задача безопаснее и удобнее для продолжения работы в текущем контексте, но может сохранять привязку к актору; detached-задача сильнее отделена и подходит для независимой работы, однако требует явно передавать безопасные данные и самостоятельно организовывать взаимодействие с акторами.
В actor-кэше после обновления записи нужно запустить асинхронную запись в журнал. Рассматривались два варианта: создать обычную Task, которая сохранит изоляцию кэша, или использовать Task.detached, передав ей снимок записи.
Обычная Task проще: она безопасно видит состояние кэша, наследует контекст и не требует ручного проектирования передачи данных. Минус — тяжёлая операция сериализации или записи может дольше занимать исполнитель актора и задерживать другие обращения к кэшу.
Task.detached лучше отделяет тяжёлую работу от актора, но требует заранее получить неизменяемый снимок данных, убедиться в его передаваемости между задачами и явно определить, как обрабатывать ошибки, отмену и завершение работы.
Выбран вариант с формированием небольшого Sendable-снимка внутри актора и последующей передачей этого снимка в независимую задачу. В результате actor не удерживается на тяжёлой обработке, а detached-задача не получает опасного доступа к его изменяемому состоянию.
1. Наследует ли обычная Task actor isolation, если она создана внутри метода актора, но начинает выполняться позже?
Да. Момент фактического запуска не меняет унаследованный контекст. Задача может быть поставлена на выполнение позже, но её тело всё равно остаётся изолированным текущим актором, если контекст был корректно выведен компилятором.
Это не означает сохранения конкретного состояния между участками выполнения: состояние актора может измениться до запуска задачи. Наследуется правило доступа и исполнитель, а не снимок всех значений.
2. Делает ли Task.detached захваченное значение безопасным для конкурентного доступа?
Нет. Detached-задача не добавляет захваченному объекту потокобезопасность и не превращает ссылочный тип в Sendable. Передавать следует значения, соответствующие требованиям конкурентной передачи, либо безопасные изолированные абстракции.
Например, неизменяемая структура может быть подходящим снимком, а обычный изменяемый класс — потенциальным источником гонки. Сам факт использования Task.detached не является механизмом синхронизации.
3. Нужно ли использовать Task.detached, чтобы гарантированно уйти с executor актора?
Нет. Task.detached убирает наследование actor isolation, но не является универсальной гарантией конкретного потока или аппаратного ядра. Планирование всё равно выполняется runtime Swift Concurrency.
Для ухода от актора важнее не захватывать его изолированное состояние в тяжёлой операции и передавать независимые данные. Если работа должна оставаться связанной с жизненным циклом родительской операции, предпочтительнее structured concurrency, а не detached-задача.