Как безопасно изменять общую переменную из нескольких потоков в Swift?

Ответ

Требуется синхронизация доступа. Без нее возникает состояние гонки (race condition), приводящее к неопределенному поведению и крашам.

1. Serial DispatchQueue (базовый подход):

let serialQueue = DispatchQueue(label: "com.app.dataQueue", qos: .userInitiated)
private var _counter = 0

var counter: Int {
    get { serialQueue.sync { _counter } }
    set { serialQueue.async(flags: .barrier) { self._counter = newValue } }
}

// Использование
serialQueue.async {
    self._counter += 1
    print("Counter: (self._counter)")
}

2. Actor (Swift 5.5+, Concurrency):

actor BankAccount {
    private var balance: Decimal = 0

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

    func withdraw(amount: Decimal) async throws {
        guard balance >= amount else { throw InsufficientFundsError() }
        balance -= amount
    }

    func currentBalance() async -> Decimal {
        return balance
    }
}

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

3. @Atomic property wrapper (ручная реализация):

import os.lock

@propertyWrapper
struct Atomic<Value> {
    private var value: Value
    private let lock = os_unfair_lock()

    init(wrappedValue: Value) {
        self.value = wrappedValue
    }

    var wrappedValue: Value {
        get {
            os_unfair_lock_lock(&lock)
            defer { os_unfair_lock_unlock(&lock) }
            return value
        }
        set {
            os_unfair_lock_lock(&lock)
            defer { os_unfair_lock_unlock(&lock) }
            value = newValue
        }
    }
}

// Использование
class SharedData {
    @Atomic var counter = 0
    @Atomic var configuration: [String: Any] = [:]
}

4. NSLock / NSCondition (Objective-C совместимость):

class ThreadSafeArray<T> {
    private var storage: [T] = []
    private let lock = NSLock()

    func append(_ element: T) {
        lock.lock()
        defer { lock.unlock() }
        storage.append(element)
    }

    var count: Int {
        lock.lock()
        defer { lock.unlock() }
        return storage.count
    }
}

Рекомендации по выбору:

  • Actor — для нового кода с async/await
  • Serial Queue — для GCD-кода, простые случаи
  • @Atomic — для отдельных свойств
  • NSLock — при работе с Objective-C кодом

Диагностика: Включите Thread Sanitizer (TSan) в схеме для обнаружения гонок данных.

Ответ 18+ 🔞

Смотри, вот тебе про синхронизацию, как есть. Ситуация, блядь, классическая: у тебя несколько потоков лезут в одну переменную, как мухи на говно, и начинается пиздец — состояние гонки, или race condition. Программа начинает вести себя как пьяный ёжик в тумане: то работает, то вылетает, то значения хуй пойми какие. Короче, доверия к такому коду — ноль ебать.

1. Serial DispatchQueue (дедовский способ, но рабочий)

let serialQueue = DispatchQueue(label: "com.app.dataQueue", qos: .userInitiated)
private var _counter = 0

var counter: Int {
    get { serialQueue.sync { _counter } }
    set { serialQueue.async(flags: .barrier) { self._counter = newValue } }
}

// Использование
serialQueue.async {
    self._counter += 1
    print("Counter: (self._counter)")
}

Суть проста, как три копейки: создаёшь очередь, которая выполняет задачи строго по одной. Всё, что связано с твоей переменной, кидаешь в эту очередь. sync — значит, ждём результата, async — отправил и пошёл дальше. Флаг .barrier для записи — это чтобы пока запись идёт, чтение ждало. Без него можно, но с ним надёжнее, как ремень безопасности.

2. Actor (модный, молодёжный, Swift 5.5+)

actor BankAccount {
    private var balance: Decimal = 0

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

    func withdraw(amount: Decimal) async throws {
        guard balance >= amount else { throw InsufficientFundsError() }
        balance -= amount
    }

    func currentBalance() async -> Decimal {
        return balance
    }
}

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

Вот это, блядь, красота! Компилятор сам следит, чтобы к свойствам актора обращались только через await. Всё изолировано, синхронизировано, и тебе даже думать об этом не надо. Чистая магия, ёпта! Если пишешь на новой конкуренси — это твой выбор.

3. @Atomic property wrapper (ручная сборка, для ценителей)

import os.lock

@propertyWrapper
struct Atomic<Value> {
    private var value: Value
    private let lock = os_unfair_lock()

    init(wrappedValue: Value) {
        self.value = wrappedValue
    }

    var wrappedValue: Value {
        get {
            os_unfair_lock_lock(&lock)
            defer { os_unfair_lock_unlock(&lock) }
            return value
        }
        set {
            os_unfair_lock_lock(&lock)
            defer { os_unfair_lock_unlock(&lock) }
            value = newValue
        }
    }
}

// Использование
class SharedData {
    @Atomic var counter = 0
    @Atomic var configuration: [String: Any] = [:]
}

Тут мы берем низкоуровневый замок os_unfair_lock. Он быстрый, легковесный. Обернул свойство — и всё, оно теперь атомарное. Читаешь, пишешь — всё под замком. defer — это чтобы даже если в середине вылетит исключение, замок всё равно отпустили. Удобная штука для отдельных переменных.

4. NSLock / NSCondition (старая гвардия, для совместимости)

class ThreadSafeArray<T> {
    private var storage: [T] = []
    private let lock = NSLock()

    func append(_ element: T) {
        lock.lock()
        defer { lock.unlock() }
        storage.append(element)
    }

    var count: Int {
        lock.lock()
        defer { lock.unlock() }
        return storage.count
    }
}

Старый добрый NSLock. Если работаешь в смешанном проекте или просто привык — почему бы и нет. Главное — не забудь его отпустить, а то deadlock получишь и будешь сидеть, как дурак, и думать, почему всё зависло.

Так что же выбрать, ёпта?

  • Actor — если твой код уже на новой конкуренси (async/await). Это путь джедая.
  • Serial Queue — если проект на GCD и нужно что-то простое и понятное.
  • @Atomic — если нужно защитить парочку конкретных свойств, а не весь объект.
  • NSLock — если вокруг тебя одни Objective-C файлы и ты чувствуешь себя археологом.

И главный совет, нахуй: включи Thread Sanitizer (TSan) в настройках схемы! Он эти гонки данных находит, как собака наркотики. Запустил — и смотри, где у тебя потоки друг другу в тапки срут. Без него ты будешь как слепой котёнок — тыкаться носом и удивляться, почему опять краш.