21 ноября, 2022. Без рубрики
Я попытаюсь определить, что делает @MainActor, а чего он не гарантирует.
Зачем?
Я продолжаю видеть туториалы и доклады, объясняющие @MainActor чем-то вроде следующей цитаты.
Изоляция от основного актора(исполнителя) выражена атрибутом MainActor.
Этот атрибут можно применить к функции или замыканию, чтобы указать, что код должен выполняться на основном акторе.
Затем мы говорим, что этот код изолирован от основного актора.
Компилятор Swift гарантирует, что код, изолированный от основного актора, будет выполняться только в основном потоке с использованием того же механизма, который обеспечивает взаимоисключающий доступ к другим акторам.
– УСТРАНЕНИЕ DATA RACES ИСПОЛЬЗУЯ SWIFT CONCURRENCY – WWDC 2022
DATA RACES
Обнаруживает несинхронизированный доступ к изменяемому состоянию в нескольких потоках.
Эта цитата просто неверная.
Компилятор Swift абсолютно на 100% не гарантирует, что код, изолированный от основного актора, будет выполняться только в основном потоке.
Ложное «обещание» гораздо опаснее, чем полное отсутствие обещания. Нет реальной документации о том, что на самом деле делает @MainActor. Лучшее, что вы можете получить, это, вероятно, взглянуть на предложения Swift Evolution.
Двумя релевантными предложениями являются: для акторов (SE-0306) и глобальных акторов (SE-0316). Я понятия не имею, являются ли они единственными актуальными или они были заменены. Нет хорошего способа, о котором бы я знал, чтобы рассказать, кроме как иметь глубокие знания о всех предложениях (которых у меня нет).
Я целую вечность твердил о том, как Apple должна документировать эту критически важную часть Swift, но они этого не сделали.
Так что я собираюсь попробовать сделать это.
Это, несомненно, будет неправильным в тонком и важном смысле!
Пожалуйста, дайте мне знать, когда вы обнаружите больше крайних случаев, и я обновлю их соответствующим образом.
Текущее обновление: XCode 14.1, 21 ноября 2022 г.
Что считается @MainActor
Начнем с очевидного: любой метод или переменная, непосредственно аннотированные с помощью @MainActor.
Далее — любой метод или переменная в классе с пометкой @MainActor — или в его расширениях.
Аналогично — любой метод или переменная в классе, один из родителей которого помечен @MainActor.
(Это происходит часто, когда вы используете UIKit и AppKit)
Что считается @MainActor – Протоколы…
Протоколы более тонкие…
Метод рассматривается как @MainActor, если
Объявление – применяются оба:
а) Протокол объявляет метод как @MainActor
b) Метод объявлен в том же блоке, где объявлено соответствие протоколу.
Применение:
c) Вызываемый объект «видится» как экземпляр протокола, а не исходный класс
В примере ниже ProtOne и ProtThree соответствуют правилам «Declaration», поэтому соответствующие методы всегда считаются @MainActor. Более тонкий случай иллюстрируется ProtTwo. Соответствие объявляется как пустое расширение, метод объявлен в основном блоке, поэтому правила «Declaration» не делают его @MainActor.
Если вы вызываете two объекта Foo, он обрабатывается как НЕ @MainActor.
Если вы вызываете two для объекта Foo, который «видится» как ProtTwo, то он обрабатывается как @MainActor.
Когда @MainActor игнорируется?
Компилятор swift пытается выяснить, когда вызываются методы @MainActor, и пытается убедиться, что они вызываются в основном потоке. Потому что это технология компиляции — она может работать только во время компиляции. Потому что она чрезвычайно сложная — вероятно, она пропускает крайние случаи. Я недостаточно осведомлен, чтобы точно сказать, почему следующие случаи на самом деле игнорируются. Некоторые кажутся динамическим кодом, который эффективно работает после проверки компилятором…
Ниже, для примеров, я использую два класса.
Bar — это «чистый swift», OBar имеет аннотации для обеспечения взаимодействия с ObjC.
Я разделил Bar и OBar, чтобы проиллюстрировать, что проблемы возникают не только при использовании совместимости @ObjC, но все примеры прекрасно работают с OBar.
mainVar и mainFunc() предназначены для вызова только в основном потоке.
В каждом случае я показываю код, в котором mainVar или mainFunc не вызываются в основном потоке.
(пожалуйста, дайте мне знать, если вы найдете больше случаев!)
- Keypaths
Keypaths полностью игнорируют Swift Concurrency.
Это вызовет mainVar в фоновом потоке
– Селекторы
Вызов селектора напрямую обходит любые проверки Swift Concurrency.
Это вызывает mainVar в фоновом потоке
- Objective С
Каждый раз, когда ваши @MainActor переменная или метод вызываются ObjectiveC, правила параллелизма игнорируются.
В этом примере Swift вызывает MyObjC в фоновом потоке, но та же проблема возникает, если Swift вызывает метод ObjC, а затем _тот_ метод перемещается в фоновый поток.
Можно даже передать чистый swift код в ObjectiveC в виде блока — и он запустится.
Он, по крайней мере, генерирует сообщение с предупреждением:
Эта ситуация очень распространена, когда у вас есть шаблон делегата и код ObjC.
Это очень упрощенный пример, но, по сути, это то, что делают многие фреймворки Apple. Вы вызываете класс, и он перезванивает вам через вашего делегата.
Код Swift здесь не генерирует предупреждений, но вызывает mainFunc в фоновом потоке.
– Любая библиотека/фреймворк с делегатом или блочным обратным вызовом (возможно)
Технически это не отдельный случай, но он достаточно важен, чтобы быть подчеркнутым.
Всякий раз, когда вы вызываете Фреймворк или Библиотеку, вы не знаете, как они реализовали свой код.
Это означает, что любой обратный вызов может использовать Objective C, или может «видеть» ваш класс как Протокол, или может использовать какой-то другой подход динамического кодирования.
Один из первых случаев, когда я столкнулся с этим в своем собственном коде, был при использовании NSAlert в MacOS.
Нажатие кнопки «ОК» иногда вызывало обратную связь в фоновом потоке.
Кодирование Apple полно примеров, где эта проблема _может_ существовать; CoreBluetooth, NSNotificationCentre, любой сторонний сетевой фреймворк, - просто несколько суперочевидных примеров.
И поведение в этих библиотеках/фреймворках может меняться от версии к версии.
Если вам нужно быть уверенным — тогда вам нужно вручную отправить свой код в основную очередь
Вывод
Swift Concurrency — ВЕЛИКОЛЕПЕН.
Теперь я использую его во всем своем коде, и это значительно упрощает асинхронную работу.
Это очень впечатляющее техническое достижение. Но это не исправит все волшебным образом — и представление о том, что это возможно, - опасно.
Я считаю постыдным, что Apple не предоставила документацию о том, что именно она гарантирует. Это должно быть освещено в кратком руководстве, возможно, с технической заметкой о более тонких деталях.
Apple не должна делать ложных заявлений о том, что она делает на переговорах WWDC.
Я надеюсь, что они предоставят надлежащую документацию, и я смогу отказаться от этой статьи…
PS: Топовый Совет…
Апдейт — Добавить флажок для предупреждений…
Вы можете добавить флажок сборки, чтобы генерировать предупреждения, когда @MainActor func/var вызывается в фоновом потоке.
Это не предотвращает вызов методов @MainActor в фоновом режиме, но, по крайней мере, при выполнении генерирует фиолетовое предупреждение, которое вы должны рассмотреть.
Флажок -Xfrontend -enable-actor-data-race-checks
Добавьте его в “Other Swift Flags”