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

«Как безопасно изменять общую переменную из нескольких потоков в Swift?» — вопрос из категории Многопоточность, который задают на 10% собеседований IOS Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Требуется синхронизация доступа. Без нее возникает состояние гонки (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) в схеме для обнаружения гонок данных.