Как обеспечить безопасный доступ к переменной в многопоточной среде?ios-142

Основные подходы к синхронизации

1. Серийные очереди

Наиболее распространённый и безопасный способ в iOS.

class SafeContainer<T> {
    private var storage = [T]()
    private let accessQueue = DispatchQueue(label: "com.example.safe.storage", attributes: .serial)

    func append(_ item: T) {
        accessQueue.async {
            self.storage.append(item)
        }
    }

    var count: Int {
        return accessQueue.sync {
            storage.count
        }
    }
}

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

  • Простота реализации
  • Автоматическое управление потоками
  • Поддержка QoS

2. Мьютексы

Низкоуровневый, но эффективный механизм.

class ThreadSafeCounter {
    private var count = 0
    private var mutex = os_unfair_lock()

    func increment() {
        os_unfair_lock_lock(&mutex)
        defer { os_unfair_lock_unlock(&mutex) }
        count += 1
    }

    func currentValue() -> Int {
        os_unfair_lock_lock(&mutex)
        defer { os_unfair_lock_unlock(&mutex) }
        return count
    }
}

3. Атомарные свойства

Кастомная property wrapper реализация.

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

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

    var wrappedValue: Value {
        get {
            lock.lock()
            defer { lock.unlock() }
            return value
        }
        set {
            lock.lock()
            defer { lock.unlock() }
            value = newValue
        }
    }
}

class SharedData {
    @Atomic var counter = 0
}

4. Акторы в Swift 5.5+

Современный подход с языковой поддержкой.

actor BankAccount {
    private var balance: Double = 0

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

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

5. Reader-Writer блокировки

Оптимизация для частого чтения и редкой записи.

class DataCache {
    private var cache = [String: Data]()
    private let queue = DispatchQueue(label: "com.example.cache", attributes: .concurrent)

    func set(data: Data, for key: String) {
        queue.async(flags: .barrier) {
            self.cache[key] = data
        }
    }

    func get(for key: String) -> Data? {
        return queue.sync {
            cache[key]
        }
    }
}

Критерии выбора подхода

  1. Частота операций:

    • Для редких записей: Reader-Writer
    • Для частых изменений: Serial Queue или Actor
  2. Производительность:

    • Самый быстрый: os_unfair_lock
    • Самый безопасный: Actors
  3. Сложность логики:

    • Простые случаи: @Atomic
    • Сложная синхронизация: Serial Queue
  4. Поддержка Swift версий:

    • До Swift 5.5: DispatchQueue
    • Swift 5.5+: Actors

Опасные антипаттерны

  1. Небезопасный Singleton:
// ПЛОХО!
class UnsafeSingleton {
    static let shared = UnsafeSingleton()
    var counter = 0 // Не защищено!
}
  1. Гонки за состоянием:
var globalCounter = 0 // Опасная глобальная переменная

DispatchQueue.concurrentPerform(iterations: 100) { _ in
    globalCounter += 1 // Race condition!
}
  1. Неявный доступ к UI не из main thread:
DispatchQueue.global().async {
    self.label.text = "New" // Крах приложения!
}

Современные best practices

  1. Используйте value-типы:

    • Структуры по умолчанию thread-safe
    • Combine с иммутабельными состояниями
  2. Принцип изоляции:

    • Локализуйте изменяемое состояние
    • Минимизируйте shared mutable state
  3. Тестирование:

    • XCTest с Thread Sanitizer
    • Стресс-тесты с concurrent access

Резюмируем:

для безопасного доступа к переменным в iOS используйте либо серийные очереди (для Objective-C совместимости), либо акторы (в Swift 5.5+). Для критичных к производительности участков применяйте os_unfair_lock, а для сложных сценариев синхронизации - reader-writer паттерн. Всегда проверяйте многопоточный код с Thread Sanitizer.