Как стать автором
Обновить
890.82
OTUS
Цифровые навыки от ведущих экспертов

Диспетчеризация в main thread с помощью MainActor

Время на прочтение6 мин
Количество просмотров8.7K
Автор оригинала: ANTOINE VAN DER LEE

MainActor — это новый атрибут из Swift 5.5, который представляет из себя глобальный актор, выполняющий свои задачи в главном потоке (main thread). При создании приложений очень важно следить за тем, чтобы задачи обновления UI выполнялись в главном потоке, что при использовании нескольких фоновых потоков (background threads) иногда может быть затруднительно. Использование атрибута @MainActor поможет вам гарантировать, что ваш UI всегда будет обновляться в главном потоке.

Если вы не очень хорошо разбираетесь в акторах (Actor) в Swift, я рекомендую прочитать мою статью Акторы в Swift: как их использовать и как предотвращать состояние гонки по данным. Глобальные акторы (Global actors) ведут себя аналогично обычным акторам, и в этой статье я не буду вдаваться в подробности того, как работают обычные акторы.

Что такое MainActor?

MainActor — это глобально уникальный актор, который выполняет свои задачи в главном потоке. Его можно применять к свойствам, методам, инстансам и замыканиям для выполнения задач в главном потоке. Он был представлен в предложении SE-0316 Global Actors как пример глобального актора (наследующего протокол GlobalActor).

Пару слов о глобальных акторах

Глобальные акторы можно рассматривать как синглтоны: существует только по одному инстансу каждого из них. На данный момент глобальные акторы работают только при включении экспериментального параллелизма (experimental concurrency). Вы можете сделать это, добавив следующее значение в «Other Swift Flags» в настройках сборки Xcode:

-Xfrontend -enable-experimental-concurrency

Как только это функция включена, мы можем определить наш собственный глобальный актор следующим образом:

@globalActor
actor SwiftLeeActor {
    static let shared = SwiftLeeActor()
}

Свойство shared является обязательным требованием протокола GlobalActor — оно гарантирует наличие инстанса глобально уникального актора. После определения вы можете использовать глобальный актор в своем проекте так же, как и другие акторы:

@SwiftLeeActor
final class SwiftLeeFetcher {
    // ..
}

Как использовать MainActor в Swift?

Глобальный актор может быть применен к свойствам, методам, замыканиям и инстансам. Например, мы могли бы добавить атрибут MainActor к модели представления (view model), чтобы заставить ее выполнять все свои задачи в главном потоке:

@MainActor
final class HomeViewModel {
    // ..
}

Используя ключевое слово nonisolated, мы можем быть уверены, что методы без потребности исполнения в главном потоке работают максимально быстро. Класс может быть аннотирован глобальным актором только в том случае, если у него нет суперкласса, суперкласс аннотирован тем же глобальным актором или суперклассом является NSObject. Подкласс класса, аннотированного глобальным актором, должен быть изолирован тем же глобальным актором.

В иных случаях мы могли бы аннотировать с помощью глобального актора отдельные свойства:

final class HomeViewModel {
    
    @MainActor var images: [UIImage] = []

}

Аннотирование свойства images @MainActor‘ом гарантирует, что оно может быть изменено только из главного потока:

Компилятор следит за выполнением требований атрибута MainActor.
Компилятор следит за выполнением требований атрибута MainActor.

Этим атрибутом также могут быть аннотированны отдельные методы:

@MainActor func updateViews() {
    // Выполняем обновление пользовательского интерфейса ..
}

И даже замыкания могут быть аннотированны на выполнение в главном потоке:

func updateData(completion: @MainActor @escaping () -> ()) {
    /// Пример диспатча для имитации поведения
    DispatchQueue.global().async {
        async {
            await completion()
        }
    }
}

Прямое использование MainActor’а

MainActor в Swift поставляется с расширением для прямого использования:

@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
extension MainActor {
    /// Выполнение данного замыкания тела на MainActor’е.
    public static func run<T>(resultType: T.Type = T.self, body: @MainActor @Sendable () throws -> T) async rethrows -> T
}

Это позволяет нам использовать MainActor непосредственно из методов, даже если мы не определили ни одно его тело с использованием этого атрибута глобального актора:

async {
    await MainActor.run {
        // Выполняем обновление пользовательского интерфейса
    }
}

Другими словами, больше нет реальной необходимости использовать DispatchQueue.main.async.

Когда следует использовать атрибут MainActor?

До Swift 5.5 вы могли определять множество операторов диспетчеризации, чтобы гарантировать, что задачи выполняются в основном потоке. Например вот так:

func fetchData(completion: @escaping (Result<[UIImage], Error>) -> Void) {
    URLSession.shared.dataTask(with: URL(string: "..some URL")) { data, response, error in
        // .. Декодируем данные в результат


        DispatchQueue.main.async {
            completion(result)
        }
    }
}

В приведенном выше примере мы почти уверены, что диспатч необходим. Однако в других случаях диспатч может быть ненужен, поскольку мы уже находимся в главном потоке. Это привело бы к избыточной диспетчеризации, которую можно было бы избежать.

В этих случаях имеет смысл определить свойства, методы, инстансы или замыкания с MainActor, чтобы убедиться, что задачи выполняются в основном потоке. Мы могли бы, например, переписать приведенный выше пример следующим образом:

func fetchData(completion: @MainActor @escaping (Result<[UIImage], Error>) -> Void) {
    URLSession.shared.dataTask(with: URL(string: "..some URL")!) { data, response, error in
        // .. Декодируем данные в результат
        let result: Result<[UIImage], Error> = .success([])  

        async {
            await completion(result)
        }
    }
}

Поскольку сейчас мы работаем с замыканием, определяемым актором, нам необходимо использовать технику async await для вызова нашего замыкания. Использование атрибута @MainActor позволяет компилятору Swift оптимизировать наш код для повышения производительности.

Выбор правильной стратегии

Используя акторы, важно выбирать правильную стратегию. В приведенном выше примере мы решили сделать замыкание актором, что означает, что кто бы ни использовал наш метод, коллбек по завершению будет выполняться с помощью MainActor. В некоторых случаях это может быть не нужно, если, например, метод, запросивший данные, также вызывается из места, где нет необходимости обрабатывать коллбек завершения в главном потоке.

В таких случаях, вероятно, лучше возложить ответственность диспетчить в нужную очередь на других разработчиков:

viewModel.fetchData { result in
    async {
        await MainActor.run {
            // Обработка результата
        }
    }
}

Заключение

Глобальные акторы — отличное дополнение к акторам в Swift. Они позволяют нам повторно использовать обычные акторы и дают возможность выполнять задачи пользовательского интерфейса более продуктивно, поскольку компилятор может оптимизировать наш код изнутри. Глобальные акторы могут использоваться для свойств, методов, инстансов и замыканий, после чего компилятор позаботится о соблюдении их требований в нашем коде.

Если вы хотите почитать больше советов по Swift, посетите страницу о Swift. Не стесняйтесь связаться со мной или написать мне в Твиттере, если у вас есть какие-либо дополнительные советы или отзывы.


Материал подготовлен в рамках курса «iOS Developer. Professional».

Всех желающих приглашаем на двухдневный онлайн-интенсив «Пишем современное iOS приложение на SwiftUI». В первый день разберем особенности создания UI с помощью данного фреймворка. Во второй — напишем бизнес-логику с помощью нативных средств (Combine). Также будем использовать новинки, представленные на WWDC 2021, в том числе и async-await.

РЕГИСТРАЦИЯ

Теги:
Хабы:
Всего голосов 2: ↑2 и ↓0+2
Комментарии0

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS