← Назад к вопросам

Можно ли изменять переменную с нескольких очередей?

2.2 Middle🔥 151 комментариев
#CI/CD и инструменты разработки

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

🧵 Можно ли изменять переменную с нескольких очередей?

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

Развёрнутое объяснение требует понимания работы потоков, памяти и механизмов синхронизации в iOS/macOS.

❓ Почему это опасно?

Представьте, что два потока одновременно пытаются увеличить значение переменной counter:

// Пример опасного кода без синхронизации
var counter = 0

DispatchQueue.global().async {
    for _ in 0..<1000 {
        counter += 1 // Поток A читает, меняет, пишет
    }
}

DispatchQueue.global().async {
    for _ in 0..<1000 {
        counter += 1 // Поток B читает, меняет, пишет
    }
}

Операция counter += 1 не атомарна. Она состоит из трёх шагов:

  1. Чтение текущего значения из памяти
  2. Увеличение значения на 1
  3. Запись нового значения обратно в память

Два потока могут выполнить эти шаги вперемешку, например:

  • Поток A читает counter = 5
  • Поток B читает counter = 5
  • Оба увеличивают до 6
  • Оба записывают counter = 6

Итог: После двух операций инкремента значение стало 6 вместо ожидаемых 7. Это состояние гонки.

🔧 Механизмы синхронизации

Для безопасного доступа к общим ресурсам из нескольких потоков используются:

1. Serial DispatchQueue (последовательная очередь)

Самая простая и эффективная стратегия в Swift — использовать собственную serial очередь для всех операций с переменной.

// Безопасный доступ через serial очередь
class ThreadSafeCounter {
    private var value = 0
    private let queue = DispatchQueue(label: "com.example.serialQueue")
    
    func increment() {
        queue.async { // Все операции идут строго последовательно
            self.value += 1
        }
    }
    
    func getValue(completion: @escaping (Int) -> Void) {
        queue.async {
            completion(self.value)
        }
    }
}

2. Actor (в Swift 5.5+)

Современный способ, встроенный в язык. Actor изолирует своё состояние и гарантирует, что к нему можно обращаться только из одного потока за раз.

actor CounterActor {
    private var value = 0
    
    func increment() {
        value += 1 // Компилятор гарантирует изоляцию
    }
    
    func getValue() -> Int {
        return value
    }
}

// Использование
let counter = CounterActor()
Task {
    await counter.increment() // await обеспечивает безопасный доступ
}

3. NSLock / os_unfair_lock

Низкоуровневые примитивы для явной блокировки.

// Пример с NSLock
class LockedCounter {
    private var value = 0
    private let lock = NSLock()
    
    func increment() {
        lock.lock()
        defer { lock.unlock() } // Гарантированно разблокирует даже при ошибке
        value += 1
    }
}

4. Атомарные операции через OSAtomic (устаревший)

Раньше использовались атомарные операции, но сейчас они deprecated в пользу более современных подходов.

📊 Сравнение подходов

МеханизмСкоростьБезопасностьСложностьSwift-стиль
Serial QueueВысокаяВысокаяСредняяХорошо
ActorСредняяВысокаяНизкаяОтлично
NSLockОчень высокаяВысокаяВысокаяУдовлетворительно
Без синхронизацииМаксимальнаяНулеваяНизкаяКатегорически нет

🚨 Особые случаи и нюансы

  1. Read-mostly данные: Если переменная в основном читается, а пишется редко, используйте concurrent очередь с барьерами:
private let concurrentQueue = DispatchQueue(
    label: "com.example.concurrent",
    attributes: .concurrent
)
private var _data: [String] = []

func updateData(_ newData: [String]) {
    concurrentQueue.async(flags: .barrier) { // Барьер = эксклюзивная запись
        self._data = newData
    }
}

func getData() -> [String] {
    var result: [String] = []
    concurrentQueue.sync { // Множественное чтение может идти параллельно
        result = self._data
    }
    return result
}
  1. Value types (структуры): Изменение структур из нескольких потоков особенно опасно, так как они копируются при каждом изменении.

  2. @MainActor: Для UI-переменных всегда используйте главный поток:

@MainActor
class ViewModel: ObservableObject {
    @Published var text: String = "" // Все изменения на главном потоке
}

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

  1. По умолчанию используйте Actor для нового кода на Swift 5.5+
  2. Для перформанс-критичных участков используйте serial очереди или примитивы синхронизации
  3. Всегда проверяйте инструментами Thread Sanitizer в Xcode
  4. Избегайте общего изменяемого состояния — проектируйте с иммутабельными структурами, когда возможно

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