Акторы и гонки данных
Изоляция акторов и реентерабельность, `Sendable`, `@MainActor` и глобальные акторы, и правила строгой конкурентности Swift 6 / 6.2, превращающие гонки данных в ошибки компиляции.
12 вопросов
JuniorТеорияОчень частоЧто такое actor и чем вызов его метода отличается от вызова метода класса?
Что такое actor и чем вызов его метода отличается от вызова метода класса?
Actor — это ссылочный тип, который защищает своё изменяемое состояние от гонок данных — к нему обращается лишь одна задача. Вызов его метода извне асинхронный: нужен await, и вызов может приостановиться, пока actor занят.
Типичные ошибки
- ✗Считают actor значимым типом, копирующим состояние каждому вызывающему
- ✗Думают, что много задач могут трогать состояние actor'а параллельно
- ✗Ждут, что вызовы методов actor'а синхронны, как у class
Уточняющие вопросы
- →Почему межакторный вызов метода требует
await, а не прямого вызова? - →Что оставляет внутренние вызовы методов самого actor'а синхронными?
MiddleТеорияОчень частоЧто означает Sendable, какие типы получают его автоматически и что вы обещаете через @unchecked Sendable?
Что означает Sendable, какие типы получают его автоматически и что вы обещаете через @unchecked Sendable?
Sendable — гарантия времени компиляции, что значение безопасно передавать через границу изоляции. Значимые типы из Sendable-полей, actor'ы и неизменяемые final class подходят. @unchecked Sendable глушит проверку — безопасность гарантируете вы сами.
Типичные ошибки
- ✗Считают
Sendableruntime-копированием, а не проверкой на этапе компиляции - ✗Думают, что любой class соответствует
Sendableавтоматически - ✗Полагают, что
@uncheckedсохраняет гарантию безопасности от компилятора
Уточняющие вопросы
- →Когда struct не становится
Sendableавтоматически? - →Что нужно добавить в class, чтобы
@unchecked Sendableбыл честным?
MiddleТеорияЧастоРеентерабельность actor'а — метод делает await в середине; какой инвариант ломается и как защитить состояние?
Реентерабельность actor'а — метод делает await в середине; какой инвариант ломается и как защитить состояние?
Actor'ы реентерабельны: пока метод ждёт на await, actor может выполнить вызов, меняющий состояние, поэтому инвариант, проверенный до него, может устареть. Защита — перечитывать состояние после каждого await или держать секцию без await.
Типичные ошибки
- ✗Считают, что actor остаётся заблокированным через
await - ✗Думают, что реентерабельность даёт deadlock, а не переплетение
- ✗Полагают, что один
@MainActorубирает опасность реентерабельности
Уточняющие вопросы
- →Почему actor отпускает изоляцию в точке приостановки, а не держит её?
- →Как переработать метод, чтобы инвариант пережил
await?
MiddleКодЧастоПреобразуйте class общего кэша с блокировкой в actor
Преобразуйте class общего кэша с блокировкой в actor
Сделайте тип actor и удалите NSLock — компилятор сам сериализует доступ. Каждое место вызова становится async, поэтому к кэшу обращаются через await. Теряете синхронный доступ: не-async код больше не может прочитать кэш напрямую без Task.
Типичные ошибки
- ✗Думают, что места вызова остаются синхронными после перехода на
actor - ✗Оставляют
NSLockвнутри actor'а, будто он всё ещё нужен - ✗Считают, что
actorпревращает кэш в значимый тип
Уточняющие вопросы
- →Как прочитать кэш actor'а из синхронного колбэка UIKit?
- →Почему компилятор делает каждое внешнее чтение кэша
await?
MiddleТеорияЧастоЧто означает @MainActor на типе или функции и что происходит при вызове из фонового контекста?
Что означает @MainActor на типе или функции и что происходит при вызове из фонового контекста?
@MainActor привязывает тип или функцию к главному actor, поэтому её состояние трогается только на главном потоке. Из фона вызов становится async: нужен await, а прямой синхронный вызов — ошибка компиляции.
Типичные ошибки
- ✗Считают
@MainActorruntime-подсказкой, а не изоляцией на этапе компиляции - ✗Думают, что фоновый вызывающий может вызвать её синхронно без
await - ✗Полагают, что она вставляет блокирующий
main.syncвнутри
Уточняющие вопросы
- →Почему вызов
@MainActorиз фоновой задачи требуетawait, а не блокировки? - →Чем
@MainActorна всём типе отличается от неё на одном методе?
MiddleДебаггингЧастоНе-Sendable передан через границу actor'а — разберите законные исправления и выберите одно
Не-Sendable передан через границу actor'а — разберите законные исправления и выберите одно
Законные исправления — сделать тип Sendable (значимый тип или неизменяемый final class); превратить его в actor; изолировать обе стороны в @MainActor; либо передать sending-значение. Лучше Sendable-значение — это убирает опасность в корне.
Типичные ошибки
- ✗Хватаются за
@unchecked Sendableкак за единственное исправление - ✗Приводят к
Any, чтобы обойти проверку - ✗Думают, что
Taskглубоко копирует захваченные значения через границу
Уточняющие вопросы
- →Когда превратить тип в
actorлучше, чем сделать егоSendable? - →Что гарантирует
sending, чего не даёт обычныйSendable?
MiddleДизайнЧастоУ вас одно общее изменяемое in-memory хранилище, читаемое и записываемое из многих мест: часть на главном потоке для UI, часть из фоновых сетевых колбэков. Коллега спрашивает, какой из трёх инструментов должен владеть его потокобезопасностью — отдельный экземпляр actor, глобальный @MainActor или приватная последовательная DispatchQueue, на которую вы делаете sync/async. Выберите один и защитите против двух других. Разберите корректность при конкурентном доступе, должны ли вызывающие стать async, цену перехода на главный поток ради данных, не связанных с UI, реентерабельность и что лучше стареет в языковом режиме Swift 6. Назовите принимаемый компромисс.
У вас одно общее изменяемое in-memory хранилище, читаемое и записываемое из многих мест: часть на главном потоке для UI, часть из фоновых сетевых колбэков. Коллега спрашивает, какой из трёх инструментов должен владеть его потокобезопасностью — отдельный экземпляр actor, глобальный @MainActor или приватная последовательная DispatchQueue, на которую вы делаете sync/async. Выберите один и защитите против двух других. Разберите корректность при конкурентном доступе, должны ли вызывающие стать async, цену перехода на главный поток ради данных, не связанных с UI, реентерабельность и что лучше стареет в языковом режиме Swift 6. Назовите принимаемый компромисс.
Выбирайте отдельный actor: проверяемая компилятором изоляция состояния, вызовы остаются async без привязки данных к UI-потоку, и он лучше стареет в режиме Swift 6. @MainActor зря гонит не-UI-работу на главный поток; последовательная DispatchQueue работает, но не проверяется и легко ломается. Компромисс — каждый доступ становится async.
Типичные ошибки
- ✗Берут
@MainActorпо умолчанию для состояния, не связанного с UI - ✗Доверяют безопасности очереди, ведь она «исполняет по одному блоку»
- ✗Игнорируют, что actor делает каждый доступ
async
Уточняющие вопросы
- →Почему
@MainActorдля не-UI-хранилища тратит время главного потока? - →Какую гарантию компилятора даёт
actor, чего не даёт последовательная очередь?
MiddleТеорияИногдаСтрогая проверка конкурентности — minimal, targeted, complete — на цели Swift 5; что проверяет каждый уровень?
Строгая проверка конкурентности — minimal, targeted, complete — на цели Swift 5; что проверяет каждый уровень?
minimal проверяет лишь код, уже перешедший на конкурентность (явный Sendable, actor'ы). targeted добавляет ваши API, принявшие её, но молчит об остальном. complete проверяет весь код по правилам гонок режима Swift 6 — его включают для постепенной миграции на Swift 5.
Типичные ошибки
- ✗Думают, что уровни настраивают runtime-блокировки, а не проверки компиляции
- ✗Считают, что
minimalсразу строго проверяет всю программу - ✗Полагают, что
completeне связан с языковым режимом Swift 6
Уточняющие вопросы
- →Зачем включать проверку
completeдо перехода языкового режима на 6? - →Какой уровень вы включите первым на большой legacy-цели?
SeniorДизайнИногдаВаша команда внедряет строгую конкурентность Swift 6 в существующем UIKit-приложении и тонет в ошибках Sendable и изоляции, большинство из которых — на обычном UI-коде, работающем только на главном потоке. Коллега указывает на Swift 6.2 / Xcode 26 "Approachable Concurrency" и его настройку сборки Default Actor Isolation и предлагает выставить её в MainActor. Объясните, что эта настройка меняет в том, где выполняются непомеченные объявления, почему она убирает большинство ошибок, что всё же придётся пометить nonisolated и зачем, и как внедрять её постепенно в многомодульном приложении, не загоняя фоновую работу на главный actor. Учтите, что она опциональна и по умолчанию выключена.
Ваша команда внедряет строгую конкурентность Swift 6 в существующем UIKit-приложении и тонет в ошибках Sendable и изоляции, большинство из которых — на обычном UI-коде, работающем только на главном потоке. Коллега указывает на Swift 6.2 / Xcode 26 "Approachable Concurrency" и его настройку сборки Default Actor Isolation и предлагает выставить её в MainActor. Объясните, что эта настройка меняет в том, где выполняются непомеченные объявления, почему она убирает большинство ошибок, что всё же придётся пометить nonisolated и зачем, и как внедрять её постепенно в многомодульном приложении, не загоняя фоновую работу на главный actor. Учтите, что она опциональна и по умолчанию выключена.
Default Actor Isolation в MainActor делает непомеченные объявления @MainActor по умолчанию, поэтому UI-код main-изолирован, убирая ошибки main-потока. Фоновую работу помечайте nonisolated или выносите в actor, держа её вне main. Внедряйте помодульно; она опциональна, по умолчанию выключена.
Типичные ошибки
- ✗Думают, что настройка включена по умолчанию или меняет поток в runtime
- ✗Полагают, что она отключает строгую проверку конкурентности
- ✗Забывают пометить фоновую работу
nonisolated, тормозя UI
Уточняющие вопросы
- →Как удержать CPU-нагруженный сервис вне главного actor'а при этой настройке?
- →Зачем внедрять Default Actor Isolation по одному модулю за раз?
SeniorТеорияИногдаnonisolated, isolated-параметры и nonisolated(unsafe) — что делает каждый и когда последний оправдан?
nonisolated, isolated-параметры и nonisolated(unsafe) — что делает каждый и когда последний оправдан?
nonisolated выводит член из-под изоляции: синхронно, но трогает лишь Sendable/неизменяемое состояние. isolated-параметр выполняет функцию в изоляции нужного actor'а. nonisolated(unsafe) глушит проверку — оправдан лишь для состояния, защищённого извне.
Типичные ошибки
- ✗Меняют местами роли
nonisolatedиisolated-параметра - ✗Считают эти аннотации подсказками оптимизатора, а не средствами изоляции
- ✗Думают, что
nonisolated(unsafe)безопасен, ведь runtime всё равно блокирует
Уточняющие вопросы
- →Какое состояние
nonisolated-метод может законно читать? - →Что должно быть верно о значении, чтобы
nonisolated(unsafe)был честным?
SeniorТеорияИногдаЧто становится жёсткой ошибкой компиляции в языковом режиме Swift 6, оставаясь лишь предупреждением в режиме Swift 5?
Что становится жёсткой ошибкой компиляции в языковом режиме Swift 6, оставаясь лишь предупреждением в режиме Swift 5?
В языковом режиме Swift 6 проверка конкурентности обязательна: нарушения гонок становятся ошибками, не предупреждениями — передача не-Sendable через границу изоляции или доступ к actor-состоянию без await. Toolchain может собрать режим Swift 5.
Типичные ошибки
- ✗Путают toolchain Swift 6 с языковым режимом Swift 6
- ✗Думают, что новые ошибки про optional'ы, а не про гонки данных
- ✗Полагают, что проверки срабатывают в runtime, а не при компиляции
Уточняющие вопросы
- →Как перевести один модуль в языковой режим Swift 6?
- →Зачем оставлять режим Swift 5 доступным в toolchain Swift 6?
SeniorТеорияРедкоНапишите свой @globalActor — когда глобальный actor лучше экземпляра actor?
Напишите свой @globalActor — когда глобальный actor лучше экземпляра actor?
@globalActor отдаёт общий shared-actor; им помечают много типов, деля один домен изоляции. Он лучше отдельного экземпляра actor, когда несвязанный код приложения должен идти сериализованно на одном executor, как @MainActor для UI.
Типичные ошибки
- ✗Считают
@globalActorлишь сокращением для экземпляраactor - ✗Думают, что он создаёт отдельный домен изоляции на каждый помеченный тип
- ✗Принимают глобальный actor за выделенный OS-поток
Уточняющие вопросы
- →Как требование
sharedделает глобальный actor единым доменом? - →Почему
@MainActor— образцовый глобальный actor?