Как безопасно обновлять массив из фонового потока в Swift?

Ответ

Да, можно обновлять массив из фонового потока, но массивы Swift не являются потокобезопасными. Прямое изменение из нескольких потоков вызывает race condition и может крашить приложение.

Решение 1: GCD с барьером (iOS 4+)

class ThreadSafeArray<T> {
    private var array: [T] = []
    private let queue = DispatchQueue(label: "com.app.threadSafeArray", 
                                    attributes: .concurrent)

    func append(_ element: T) {
        queue.async(flags: .barrier) {
            self.array.append(element)
            // Обновление UI должно быть на main
            DispatchQueue.main.async {
                // UI обновления здесь
            }
        }
    }

    var values: [T] {
        queue.sync { array } // Безопасное чтение
    }
}

Решение 2: Actor (Swift 5.5+, iOS 13+)

actor SafeArray<T> {
    private var array: [T] = []

    func append(_ element: T) {
        array.append(element)
    }

    func getAll() -> [T] {
        return array
    }
}

// Использование:
let safeArray = SafeArray<String>()
Task {
    await safeArray.append("Item from background")
    let items = await safeArray.getAll()
}

Решение 3: @MainActor для UI-обновлений

@MainActor
class ViewModel: ObservableObject {
    @Published var items: [String] = []

    func fetchData() async {
        let newItems = await fetchFromBackground()
        // Автоматически на главном потоке благодаря @MainActor
        items.append(contentsOf: newItems)
    }
}

Критические правила:

  1. Все модификации массива должны быть синхронизированы
  2. Чтение тоже требует синхронизации (race condition возможен при чтении во время записи)
  3. Для UI-обновлений всегда переключайтесь на главный поток
  4. Используйте NSLock или os_unfair_lock для микрооптимизаций
  5. Рассмотрите готовые решения вроде Atomic из Swift Collections

Ответ 18+ 🔞

О, слушай, смотри, вот тут такая история, блядь. Ты там в фоне массивы пытаешься пинать, да? Ну, типа, пока юзер пальцем по экрану водит, ты в тихую, на заднем плане, данные подгружаешь и в массив их суёшь. Так вот, запомни раз и навсегда, как отче наш: массивы в Swift — это не потокобезопасные штуки, блядь! Возьмёшь и начнёшь с разных потоков в одну кучу лезть — получишь race condition, а потом будешь чесать репу, отчего твое приложение накрылось медным тазом и вылетело с крашем. Пиздец, а не отладка.

Ладно, не кипятись. Вот тебе несколько способов, как это сделать, чтобы не обосраться.

Вариант первый: Старая добрая GCD с барьером (для дедов, у которых проект с iOS 4+)

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

class ThreadSafeArray<T> {
    private var array: [T] = []
    private let queue = DispatchQueue(label: "com.app.threadSafeArray", 
                                    attributes: .concurrent)

    func append(_ element: T) {
        queue.async(flags: .barrier) {
            self.array.append(element)
            // Слушай, а UI-то обновлять надо на главном, ёпта! Не забудь!
            DispatchQueue.main.async {
                // Вот тут уже безопасно ткнуть интерфейс
            }
        }
    }

    var values: [T] {
        queue.sync { array } // Читаем тоже безопасно, а то мало ли
    }
}

Вариант второй: Модный Actor (Swift 5.5+, iOS 13+)

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

actor SafeArray<T> {
    private var array: [T] = []

    func append(_ element: T) {
        array.append(element)
    }

    func getAll() -> [T] {
        return array
    }
}

// Используем:
let safeArray = SafeArray<String>()
Task {
    await safeArray.append("Item from background")
    let items = await safeArray.getAll()
}

Вариант третий: @MainActor для тех, кто любит, чтобы UI сам обновлялся

Помечаешь весь класс или метод @MainActor — и всё, что в нём происходит, автоматически летит на главный поток. Удобно, когда у тебя ViewModel и надо, чтобы @Published свойства не взрывались.

@MainActor
class ViewModel: ObservableObject {
    @Published var items: [String] = []

    func fetchData() async {
        let newItems = await fetchFromBackground() // Это где-то в фоне
        // А вот эта строка уже автоматом на главном, потому что весь класс на @MainActor
        items.append(contentsOf: newItems)
    }
}

А теперь, блядь, главные правила, которые надо выжечь на подкорке:

  1. Любое изменение массива из нескольких потоков должно быть синхронизировано. Без вариантов. Иначе — рандомные краши, потерянные данные, и ты будешь искать баг неделю, а он в одной строчке.
  2. Чтение — тоже операция, ёпта! Если один поток пишет, а другой в этот момент читает — это тоже race condition. Так что читай через тот же механизм синхронизации.
  3. UI обновлять — только на главном потоке. Сколько уже можно об этом говорить? Даже если данные пришли из фона, перекинь обновление интерфейса в DispatchQueue.main.async.
  4. Если нужна супер-производительность (миллионы операций в секунду, хотя хрен ты такое упрёшь в мобильном приложении), можешь ковыряться с NSLock или os_unfair_lock. Но сначала убедись, что барьер или актор — это реально бутылочное горлышко.
  5. Не изобретай велосипед. Посмотри в сторону готовых решений, типа Atomic из пакета Swift Collections. Может, там уже всё придумали за тебя.

Вот и вся магия, блядь. Главное — не лезь с бульдозером в тонкий механизм, а то получишь ебаный клубок из гонок данных и нестабильного интерфейса. Делай по правилам, и будет тебе счастье.