Connect with us

Программирование

Акторы в Swift: руководство для начинающих по безопасному параллелизму

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

Опубликовано

/

     
     

У каждого бага есть своя история, но гонки данных — это настоящие ужастики в мире iOS-разработки.

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

Старые методы — такие как блокировки (locks) и очереди GCD — иногда помогают, но они ненадёжны. Пропущенная блокировка или неправильный вызов очереди — и всё рушится.

Речь здесь не о скорости кода, а о его безопасности.

Акторы (actor) в Swift — это встроенный подход Apple к безопасной работе с разделяемым состоянием в многопоточной среде. Они предотвращают гонки данных, делают код читаемым и позволяют компилятору автоматически следить за соблюдением правил безопасности.

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

Перевод для iOS-разработчика на русском языке:

Что такое актор в Swift?

Если объяснять просто: актор в Swift — это что-то вроде class… но с телохранителями.

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

  • Значение вдруг становится неправильным
  • UI показывает устаревшие данные
  • Или, что хуже, приложение внезапно падает

Это называется гонка данных (data race), и это один из самых сложных багов — его невозможно стабильно воспроизвести.

Акторы — это способ Swift сказать:

Только один код за раз может работать с моими данными!

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

Пример:

actor Counter {
    private var value = 0
    
    func increment() {
        value += 1
    }
    
    func getValue() -> Int {
        value
    }
}

let counter = Counter()

Task {
    await counter.increment()
    print(await counter.getValue()) // Prints: 1
}

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

  • Counter — это актор, хранящий число
  • Даже если 10 задач одновременно попытаются вызвать increment(), Swift выполнит их по одной
  • Вы используете await при обращении к актору извне, потому что вам может потребоваться «дождаться своей очереди»

Помните:

  • Актор — это как безопасная комната для ваших данных
  • Одновременно в неё может войти только один человек (задача)
  • Вам не нужно думать о блокировках, очередях или семафорах, Swift делает это за вас

Почему бы просто не использовать блокировки или GCD?

До акторов iOS-разработчики часто использовали блокировки (NSLock, DispatchSemaphore) или очереди (DispatchQueue) для обеспечения потокобезопасности кода. Они работают, но с ними связаны проблемы.

1. Блокировки могут быть сложными

При использовании блокировки необходимо помнить о блокировке перед изменением данных и разблокировке после. Если вы забудете разблокировать или заблокировать в неправильном порядке, приложение может зависнуть. Это называется deadlock.

let lock = NSLock()
var count = 0

func increment() {
    lock.lock()
    count += 1
    lock.unlock()
}

Если пропустить хотя бы один метод unlock(), всё приложение может перестать отвечать.

2. Очереди GCD могут быть запутанными

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

let queue = DispatchQueue(label: "CounterQueue")
var count = 0

func increment() {
    queue.sync {
        count += 1
    }
}

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

3. Акторы решают обе проблемы

Акторы обеспечивают автоматическую изоляцию и встроены непосредственно в систему параллельного выполнения Swift. Вам не нужно писать вызовы блокировки/разблокировки и вручную управлять очередями. Компилятор обеспечивает безопасность за вас, поэтому вы можете сосредоточиться на логике приложения, а не на правилах потоковой обработки.

Если задуматься:

  • Блокировки = вы сами управляете дверью
  • Очереди GCD = вы поручаете кому-то охранять дверь
  • Акторы = дверь уже охраняется Swift, и вы просто входите, когда подходит ваша очередь

Использование акторов с Async/Await

Акторы лучше всего работают с системой async/await Swift.

Когда вы вызываете метод актора извне, вы добавляете await, потому что вам может потребоваться дождаться своей очереди. Именно так Swift обеспечивает безопасный доступ к данным актора.

Пример:

actor UserData {
    private var users: [String] = []
    
    func addUser(_ name: String) {
        users.append(name)
    }
    
    func getUsers() -> [String] {
        users
    }
}

let userData = UserData()

Task {
    await userData.addUser("Alice")
    await userData.addUser("Bob")
    print(await userData.getUsers()) // ["Alice", "Bob"]
}

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

  • addUser и getUsers запускаются по одному, даже если их вызывают несколько задач одновременно
  • Ключевое слово await приостанавливает выполнение кода до готовности актора
  • Внутри актора вы можете читать и записывать свойства напрямую, без ожидания

Совет: если вы забудете await при вызове метода актора извне, Xcode выдаст ошибку — способ Swift напомнить вам о необходимости соблюдать безопасность.

Глобальные акторы

Глобальный актор — это одна общая «линия изоляции», которую могут использовать несколько типов. Всё, что отмечено одним и тем же глобальным актором, выполняется в этой линии, по одному за раз. Представьте себе общую очередь с проверкой безопасности компилятором.

Пример: глобальный актор кастомной базы данных

@globalActor
struct DatabaseActor {
    actor ActorType {}
    static let shared = ActorType()
}

// Use the global actor for all database work
@DatabaseActor
class DatabaseManager {
    func fetchData() {
        print("Fetching data safely from the database...")
    }
}

let db = DatabaseManager()

Task {
    await db.fetchData() // Safe, always runs in DatabaseActor lane
}

Почему они важны

  • Общий контекст: весь доступ к базе данных или все аналитические записи размещаются в одном глобальном акторе, чтобы избежать конфликтов
  • Согласованность: все, кто использует этот актор, играют по одним и тем же правилам
  • Ясность: ваш код демонстрирует намерение: «эта работа должна быть общей»

Встроенный актор, который вы уже используете

  • @MainActor — это глобальный актор для работы с пользовательским интерфейсом. Он обеспечивает выполнение обновлений пользовательского интерфейса в основном потоке, обеспечивая стабильность и бесперебойность работы приложения.

Когда создавать свой собственный актор

  • Уровень данных: всё сохранение данных и кэширование через @DatabaseActor
  • Телеметрия: всё отслеживание событий через @AnalyticsActor
  • Конфигурация: все флаги функций и настройки через @ConfigActor

Миграция синглтона в актор

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

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

Старый синглтон (не актор)

class Logger {
    static let shared = Logger()
    private init() {}
    
    func log(_ message: String) {
        print("[LOG]: \(message)")
    }
}

Проблемы:

  • Отсутствует встроенная потокобезопасность
  • Если логирование происходит из разных потоков одновременно, выходные данные могут смешиваться

Новый синглтон (актор)

actor Logger {
    static let shared = Logger()
    private init() {}
    
    func log(_ message: String) {
        print("[LOG]: \(message)")
    }
}

Преимущества:

  • Потокобезопасность обеспечивается автоматически: Swift гарантирует, что одновременно выполняется только одна запись в лог
  • Блокировки и очереди отправки не требуются
  • API выглядит практически так же, поэтому миграция проста

Когда использовать акторы (краткий чеклист)

  • Общее изменяемое состояние, используемое из нескольких потоков
  • Уровни кэширования и хранения
  • Модели данных обновляются фоновыми задачами
  • Функции многопользовательской совместной работы
  • Длительные задачи с отслеживанием хода выполнения

Ограничения и подводные камни

Акторы — мощные инструменты, но не волшебные. Вот несколько моментов, которые следует учитывать:

1. Они по-прежнему являются ссылочными типами

Под капотом акторы работают как классы, поэтому следуют ARC (автоматическому подсчёту ссылок). Даже если вы сохраняете строгие ссылки везде, вы всё равно можете создавать циклы удержания.

2. Нельзя пропустить await при вызове извне

Если вы вызываете метод актора или читаете свойство извне, Swift заставит вас использовать await. Так обеспечивается безопасность. Забыть await невозможно, так как компилятор вас остановит.

3. Прямой доступ к свойству извне запрещён

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

4. nonisolated обходит безопасность

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

5. Слишком много «переходов между акторами» может замедлить работу

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

6. Не требуется для простых локальных данных

Если значение используется только внутри одного метода или структуры и никогда не передается между потоками, вам не нужен актор. Акторы решают конкретную задачу, используют общее изменяемое состояние и не должны использоваться слишком часто.

Реальные примеры использования акторов в iOS

  1. Менеджер сетевого кэша — предотвращает состояние гонки, когда несколько представлений запрашивают одни и те же данные
  2. Контроллер игрового состояния — обеспечивает согласованность счёта, здоровья и инвентаря
  3. Аналитический логер — безопасно записывает события из разных частей приложения
  4. Менеджер загрузок — координирует одновременные загрузки, не повреждая данные в ходе выполнения

Заключение

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

Начните с одного общего ресурса, перенесите его в актор и вызовите его с await. Вы заметите меньше ошибок и более чистый код.

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

Источник

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.
Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: