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

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

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

Что такое MainActor?

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

Понимание глобальных участников

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

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

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

@SwiftLeeActor
final class SwiftLeeFetcher {
    // ..
}

Везде, где вы используете глобальный атрибут actor, вы обеспечите синхронизацию через общий экземпляр actor, чтобы обеспечить взаимоисключающий доступ к объявлениям. Результат аналогичен акторам в целом, как описано в разделе «Актеры в Swift: как использовать и предотвращать гонки данных».

Базовая реализация @MainActor аналогична нашей пользовательской реализации @SwiftLeeActor:

@globalActor
final actor MainActor: GlobalActor {
    static let shared: MainActor
}

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

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

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

@MainActor
final class HomeViewModel {
    // ..
}

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

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

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

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

Вы также можете пометить атрибутом отдельные методы:

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

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

func updateData(completion: @MainActor @escaping () -> ()) {
    Task {
        await someHeavyBackgroundOperation()
        await completion()
    }
}

Хотя в этом случае вам следует переписать метод updateData на асинхронный вариант без необходимости замыкания завершения.

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

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

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 непосредственно из методов, даже если мы не определяли ни одно из его тел с помощью атрибута глобального актора:

Task {
    await someHeavyBackgroundOperation()
    await MainActor.run {
        // Perform UI updates
    }
}

Другими словами, больше нет необходимости использовать DispatchQueue.main.async. Однако я рекомендую использовать глобальный атрибут, чтобы ограничить любой доступ к основному потоку. Без атрибута глобального актора любой может забыть использовать MainActor.run, что может привести к обновлению пользовательского интерфейса в фоновом потоке.

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

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

func fetchImage(for url: URL, completion: @escaping (Result<UIImage, Error>) -> Void) {
    URLSession.shared.dataTask(with: url) { data, response, error in
        guard let data, let image = UIImage(data: data) else {
            DispatchQueue.main.async {
                completion(.failure(ImageFetchingError.imageDecodingFailed))
            }
            return
        }

        DispatchQueue.main.async {
            completion(.success(image))
        }
    }.resume()
}

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

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

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

@MainActor
func fetchImage(for url: URL) async throws -> UIImage {
    let (data, _) = try await URLSession.shared.data(from: url)
    guard let image = UIImage(data: data) else {
        throw ImageFetchingError.imageDecodingFailed
    }
    return image
}

Атрибут @MainActor обеспечивает выполнение логики в основном потоке, в то время как сетевой запрос все еще выполняется в фоновой очереди. Отправка главному актеру происходит только в случае необходимости для обеспечения наилучшей производительности.