Объяснение использования MainActor в Swift для отправки в основной поток

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

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

Что такое MainActor?

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

Понимание Global actors

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

-Xfrontend -enable-experimental-concurrency

Один включенный, мы могли бы определить нашего собственного GlobalActor следующим образом:

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

Общее свойство является требованием протокола GlobalActor и гарантирует наличие глобально уникального экземпляра субъекта. После определения вы можете использовать GlobalActor в своем проекте так же, как и с другими actors.

@SwiftLeeActor
final class SwiftLeeFetcher {
    // ..
}

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

Global actor может использоваться со свойствами, методами, замыканиями и экземплярами. Например, мы могли бы добавить атрибут Main Actor в модель представления, чтобы она выполняла все свои задачи в основном потоке:

@MainActor
final class HomeViewModel {
    // ..
}

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

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

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

Пометка свойства images с помощью свойства @MainActor гарантирует, что его можно будет обновить только из основного потока:

Компилятор применяет требования к атрибуту MainActor.

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

@MainActor func updateViews() {
    // Perform UI updates..
}

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

func updateData(completion: @MainActor @escaping () -> ()) {
    /// Example dispatch to mimic behaviour
    DispatchQueue.global().async {
        async {
            await completion()
        }
    }
}

Использование MainActor напрямую

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

@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
extension MainActor {

    /// Execute the given body closure on the main actor.
    public static func run<T>(resultType: T.Type = T.self, body: @MainActor @Sendable () throws -> T) async rethrows -> T
}

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

async {
    await MainActor.run {
        // Perform UI updates
    }
}

Другими словами, больше нет необходимости использовать 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
        // .. Decode data to a result
        
        DispatchQueue.main.async {
            completion(result)
        }
    }
}

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

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

func fetchData(completion: @MainActor @escaping (Result<[UIImage], Error>) -> Void) {
    URLSession.shared.dataTask(with: URL(string: "..some URL")!) { data, response, error in
        // .. Decode data to a result
        let result: Result<[UIImage], Error> = .success([])
        
        async {
            await completion(result)
        }
    }
}

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

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

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

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

viewModel.fetchData { result in
    async {
        await MainActor.run {
            // Handle result
        }
    }
}

Продолжаем свое путешествие в Swift Concurrency

Изменения параллелизма — это больше, чем просто async-await, они включают в себя множество новых функций, которые вы можете использовать в своем коде. Итак, пока вы этим занимаетесь, почему бы не погрузиться в другие функции параллелизма?

Заключение

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

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

Спасибо!