Перейти к содержанию

5. Global Actor

Мы уже с вами изучили как работает Actor, для чего он нужен. Но скорее всего вы уже слышали про MainActor. Так вот он реализован он немного по-другому. У него гораздо больше ответственности и нагрузки на него, но не потому что он отвечает за главный поток, а потому что он является глобальным(Global)

Определение

Global Actor в Swift — это механизм, который позволяет обеспечить безопасный и синхронизированный доступ к глобальным данным и функциям в многопоточной среде, используя модель акторов для контроля доступа. Если говорить тезисно:

  • Это синглтон
  • Умеет шарить своего исполнителя между вызовами
  • Умеет шарить изоляцию между всеми вызовами
  • Глобальный Actor можно применять к классам, методам, функциям, замыканиям

Примеры

@globalActor
struct MyGlobalActor: GlobalActor {
    static var shared = MyActor()

    private init() { }
}

@MyGlobalActor
func globalFunction() {
    // Действия, безопасно выполняемые в MyGlobalActor
}
При создании global Actor важно:

1) Отнаследоваться от GlobalActor

2) Добавить аннотацию @globalActor

3) Заприватить инициализатор, чтобы доступ был как к синглтону

Зачем нужен global Actor?

  1. Выполнять произвольные методы в коде последовательно
  2. Связать произвольные методы с определённым потоком
  3. Вынести определённые методы из общего исполнителя

Приведём ещё примеров:

@globalActor
private actor SharedActor: GlobalActor {
    static let shared = SharedActor()
    private init() {}
}

Task.detached { @SharedActor in
    for _ in 0..<10 {
        print("one")
    }
}

Task.detached { @SharedActor in
    for _ in 0..<10 {
         print("two")
    }
}
В примере выше будет последовательно выведено подряд либо десять единиц, а затем 10 двоек, либо наоборот. Это как раз связано с тем, что эктор выполнит одну задачу, а затем перейдёт к другой. Если же убрать @SharedActor из кложуры - то единицы и двойки будут выводиться смешанно.

MainActor

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

Task.detached { @MainActor in
    for _ in 0..<10 {
        print("one")
    }
}

Task.detached { @MainActor in
    for _ in 0..<10 {
         print("two")
    }
}
Результат будет таким же как в примере выше, только все циклы будут работать на главном потоке.

Task.detached { @MainActor in
    /// контекст главного исполнителя
    let value = await fetchValueFromStorage() /// -> уходим из MainActor
    /// Снова возвращаемся к контексту главного исполнителя
}
Task.detached {
    let value = await fetchValueFromStorage()
    await MainActor.run {
        /// запускаем задачи на MainActor
    }
}

taskFromInterview.gif

final class MyClass {

    @MainActor
    func callMainActorMethod () {
        print(Thread.isMainThread ? "Main Thread" : "Background Thread")
    }
}

struct Example {

    func execute() {
        DispatchQueue.global(qos: .background).async {
            let myClass = MyClass()
            myClass.callMainActorMethod()
        }
    }
}

let example = Example()
example.example()
Что будет выведено на экран?

Ответ: Хоть метод и помечен @MainActor и кажется, что он будет исполняться на главном потоке. Но это не так, на самом деле напечатается Background Thread. Здесь проблема вызвана именно сочетанием кода GCD и Structured Concurrency. Почему так происходит и где теряется контекст - обсудим в статьях более сложного уровня.