Actors для Swift: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Actors в Swift: Полное руководство по безопасной конкурентности

1. Что это и зачем нужно

Actors — это фундаментальная концепция языка Swift, введённая в Swift 5.5, которая решает одну из самых сложных проблем в программировании: безопасную работу с общим изменяемым состоянием в многопоточной среде. До появления Actors разработчики были вынуждены вручную управлять блокировками, семафорами и очередями диспетчеризации (DispatchQueue), что приводило к трудноуловимым ошибкам, таким как гонки данных (data races) и взаимные блокировки (deadlocks). Actors предоставляют встроенный на уровне языка механизм изоляции состояния, гарантируя, что доступ к данным внутри actor'а всегда происходит последовательно, даже если к нему обращаются из нескольких потоков одновременно.

Основная идея Actors заключается в том, что каждый actor защищает своё собственное состояние. Внешний код не может напрямую обращаться к хранимым свойствам actor'а — только через его методы. Компилятор Swift строго следит за этим, выдавая ошибку, если вы пытаетесь прочитать или изменить свойство actor'а извне без использования ключевого слова await. Это делает модель Actors не просто библиотекой, а частью системы типов языка, что позволяет обнаруживать потенциальные проблемы с конкурентностью на этапе компиляции, а не во время выполнения.

Зачем это нужно? В современной разработке под iOS, macOS, watchOS и tvOS практически любое приложение работает с сетью, базами данных или пользовательским интерфейсом, что требует асинхронных операций. Actors позволяют писать конкурентный код, который легко читать, поддерживать и отлаживать. Они особенно полезны при работе с общими ресурсами, такими как кэши, менеджеры состояния, пулы соединений и любые другие объекты, к которым возможен одновременный доступ из разных частей приложения. Actors — это не просто "ещё один инструмент", а фундаментальный сдвиг в том, как Swift-разработчики думают о конкурентности.

2. Установка

Actors являются встроенной частью языка Swift, начиная с версии 5.5. Для их использования не требуется установка каких-либо внешних библиотек или пакетов через менеджеры зависимостей (CocoaPods, Swift Package Manager, Carthage). Всё, что вам нужно — это проект, настроенный на использование Swift 5.5 или новее.

Проверка версии Swift

Убедитесь, что у вас установлена подходящая версия компилятора. Для этого выполните в терминале:

swift --version

Вы должны увидеть строку, содержащую Swift version 5.5 или выше. Если версия ниже, обновите Xcode (для macOS) или установите актуальный инструментарий Swift с официального сайта.

Настройка проекта

Если вы используете Xcode, убедитесь, что в настройках проекта (Build Settings) параметр Swift Language Version установлен в Swift 5.5 или выше. Для проектов на Swift Package Manager (SPM) минимальная версия платформы должна быть .iOS(.v15), .macOS(.v12), .tvOS(.v15) или .watchOS(.v8) — именно с этих версий Apple начала полноценную поддержку Actors в своих фреймворках.

Пример конфигурации Package.swift:

// swift-tools-version:5.7
import PackageDescription

let package = Package(
    name: "MyActorProject",
    platforms: [
        .iOS(.v15),
        .macOS(.v12)
    ],
    dependencies: [],
    targets: [
        .target(
            name: "MyActorProject",
            dependencies: []),
        .testTarget(
            name: "MyActorProjectTests",
            dependencies: ["MyActorProject"])
    ]
)

Всё готово. Никаких дополнительных команд pip install или npm install не требуется.

3. Быстрый старт — минимальный рабочий пример

Создадим простой actor, который управляет счётчиком. Это классический пример, демонстрирующий, как Actors решают проблему гонки данных.

import Foundation

// 1. Объявляем actor с помощью ключевого слова 'actor'
actor Counter {
    // 2. Состояние actor'а — приватное свойство
    private var value: Int = 0

    // 3. Метод для увеличения счётчика
    func increment() {
        value += 1
    }

    // 4. Метод для получения текущего значения
    // Обратите внимание: метод асинхронный, так как он читает изолированное состояние
    func getValue() -> Int {
        return value
    }
}

// 5. Использование actor'а
Task {
    let counter = Counter()

    // 6. Вызов методов actor'а требует 'await', так как это точки потенциального переключения контекста
    await counter.increment()
    await counter.increment()
    let currentValue = await counter.getValue()

    print("Значение счётчика: \(currentValue)") // Выведет: Значение счётчика: 2
}

// Для запуска в playground или командной строке нужно дождаться выполнения Task
RunLoop.main.run(until: Date(timeIntervalSinceNow: 1))

Что здесь происходит?

  • actor Counter — объявляет новый тип actor'а. Все его свойства и методы по умолчанию изолированы.
  • private var value — состояние, которое защищено actor'ом. Прямой доступ извне (counter.value) вызовет ошибку компиляции.
  • func increment() и func getValue() — методы, которые работают с изолированным состоянием. Они не помечены async, но их вызов извне всё равно требует await, потому что они являются частью actor'а.
  • await counter.increment() — ключевое слово await указывает, что выполнение может быть приостановлено в этой точке, пока actor обрабатывает запрос. Это гарантирует, что доступ к value будет последовательным.

4. Основные методы, функции и классы

В отличие от библиотек, Actors не предоставляют "методов API" в классическом понимании. Вместо этого они определяют набор правил и ключевых слов, которые формируют модель программирования. Ниже перечислены 8 ключевых концепций и инструментов, необходимых для работы с Actors.

4.1. Ключевое слово actor

Сигнатура: actor MyActor { ... }

Что делает: Объявляет новый тип actor'а. Все хранимые свойства и методы внутри этого типа по умолчанию изолированы от внешнего мира. Компилятор гарантирует, что доступ к ним возможен только через асинхронные вызовы (await) или изнутри самого actor'а.

actor BankAccount {
    var balance: Double = 0.0

    func deposit(amount: Double) {
        balance += amount
    }
}

// Использование
let account = BankAccount()
await account.deposit(amount: 100.0)

4.2. Ключевое слово await

Сигнатура: await expression

Что делает: Приостанавливает выполнение текущего контекста (например, Task) до тех пор, пока асинхронная операция не будет завершена. При вызове метода actor'а await сигнализирует, что код ожидает доступа к изолированному ресурсу. Это точка, в которой может произойти переключение потоков.

actor DataLoader {
    func loadData() -> String {
        // Симуляция загрузки
        return "Данные"
    }
}

Task {
    let loader = DataLoader()
    let data = await loader.loadData() // Здесь выполнение приостановится
    print(data)
}

4.3. Ключевое слово nonisolated

Сигнатура: nonisolated func myFunc() { ... } или nonisolated let property: Type

Что делает: Позволяет объявить метод или вычисляемое свойство внутри actor'а как неизолированное. Это означает, что к нему можно обращаться без await. Используется для методов, которые не зависят от изменяемого состояния actor'а (например, константы или чистые функции).

actor Calculator {
    nonisolated func add(_ a: Int, _ b: Int) -> Int {
        return a + b // Не использует состояние actor'а
    }

    func multiply(by factor: Int) -> Int {
        // Этот метод изолирован и может использовать состояние
        return factor * 2
    }
}

let calc = Calculator()
let sum = calc.add(3, 4) // Не требует await!
print(sum) // 7

4.4. Ключевое слово isolated

Сигнатура: func myFunc(isolated param: MyActor) { ... }

Что делает: Используется в параметрах функций (обычно глобальных или методах других типов) для указания, что параметр является actor'ом, и функция будет работать в его контексте изоляции. Это позволяет писать функции, которые временно получают доступ к состоянию actor'а.

actor Logger {
    var log: [String] = []

    func append(_ message: String) {
        log.append(message)
    }
}

// Глобальная функция, которая временно изолируется для работы с Logger
func writeToLog(_ logger: isolated Logger, message: String) {
    // Мы можем напрямую обращаться к свойствам logger, так как функция изолирована
    logger.log.append("[\(Date())] \(message)")
}

Task {
    let logger = Logger()
    await writeToLog(&logger, message: "Старт системы")
    let count = await logger.log.count
    print("Сообщений в логе: \(count)") // 1
}

4.5. Ключевое слово async

Сигнатура: func myFunc() async -> ReturnType

Что делает: Помечает функцию как асинхронную. Хотя методы actor'а не требуют явного async (они становятся асинхронными по умолчанию для внешних вызывающих), это ключевое слово необходимо для обычных функций, которые хотят использовать await внутри себя или быть вызванными с помощью await.

actor NetworkManager {
    func fetchData() -> String {
        return "Сетевые данные"
    }
}

// Обычная асинхронная функция
func processNetworkData() async {
    let manager = NetworkManager()
    let data = await manager.fetchData()
    print("Обработано: \(data)")
}

Task {
    await processNetworkData()
}

4.6. Структура Task

Сигнатура: Task(priority: TaskPriority? = nil, operation: @Sendable () async -> Success)

Что делает: Создаёт новую асинхронную единицу работы (task). Task является основным способом запуска асинхронного кода. Он может быть использован для вызова методов actor'а из синхронного контекста (например, из viewDidLoad).

actor UserSettings {
    var theme: String = "Светлая"

    func setTheme(_ newTheme: String) {
        theme = newTheme
    }
}

// В синхронном контексте (например, в UIViewController)
func viewDidLoad() {
    let settings = UserSettings()

    // Запускаем асинхронную операцию
    Task {
        await settings.setTheme("Тёмная")
        let currentTheme = await settings.theme
        print("Тема установлена: \(currentTheme)")
    }
}

4.7. Протокол Sendable

Сигнатура: protocol Sendable { }

Что делает: Протокол, который указывает, что значение типа можно безопасно передавать между различными контекстами конкурентности (например, между actor'ами или в Task). Компилятор проверяет соответствие этому протоколу, чтобы предотвратить случайную передачу небезопасных ссылок (например, классов без блокировок).

// Структуры и перечисления автоматически соответствуют Sendable
struct UserInfo: Sendable {
    let id: Int
    let name: String
}

actor UserDatabase {
    private var users: [Int: UserInfo] = [:]

    func addUser(_ user: UserInfo) {
        users[user.id] = user
    }
}

Task {
    let db = UserDatabase()
    let newUser = UserInfo(id: 1, name: "Alice")
    await db.addUser(newUser) // UserInfo — Sendable, передача безопасна
}

4.8. Ключевое слово globalActor

Сигнатура: @globalActor actor MyGlobalActor { ... }

Что делает: Позволяет определить глобальный actor, который может быть использован для изоляции нескольких типов или функций по всему проекту. Это полезно для управления доступом к глобальным ресурсам, таким как база данных или файловая система, без необходимости передавать экземпляр actor'а повсюду.

@globalActor actor DatabaseActor {                    

Рекомендаций