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

Приведи пример интересного бага

2.0 Middle🔥 231 комментариев
#Soft Skills и карьера#Тестирование и отладка

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

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

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

Распространение неожиданного состояния через shared mutable object в конкурентной среде

Один из самых интересных и коварных багов, с которыми я сталкивался, связан с неявным совместным использованием изменяемого состояния в многопоточной среде iOS-приложения. Ошибка возникала в социальном приложении, где пользователь мог просматривать ленту контента, а система периодически обновляла данные в фоне.

Контекст проблемы

В архитектуре использовался паттерн Repository с общим кэшем в памяти (синглтон FeedCacheManager). Кэш хранил массив объектов FeedItem, которые отображались в UICollectionView. При обновлении данных из сети, фоновый поток модифицировал этот массив, в то время как главный поток отрисовывал интерфейс на его основе.

Код с ошибкой

class FeedCacheManager {
    static let shared = FeedCacheManager()
    private var cachedItems: [FeedItem] = []
    
    func updateItems(_ newItems: [FeedItem]) {
        // Фоновый поток выполняется здесь
        cachedItems = newItems
    }
    
    func getItems() -> [FeedItem] {
        return cachedItems
    }
}

class FeedViewController: UIViewController {
    private var displayedItems: [FeedItem] = []
    
    func refreshUI() {
        // Главный поток выполняется здесь
        displayedItems = FeedCacheManager.shared.getItems()
        collectionView.reloadData()
    }
}

Проявление бага

В определенных условиях (обычно при быстром скролле во время фонового обновления) приложение неожиданно крашилось с ошибкой:

Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayM objectAtIndex:]: index X beyond bounds [0 .. Y]'

При этом X часто был равен Y+1, что указывало на рассинхронизацию между данными и интерфейсом.

Коренная причина

После нескольких дней отладки с использованием Thread Sanitizer и тщательного логирования, обнаружилась последовательность:

  1. Фоновый поток начинал замену cachedItems новым массивом
  2. В момент присваивания cachedItems = newItems происходила частичная замена ссылки
  3. Главный поток в методе cellForItem(at:) запрашивал getItems() и получал гибридное состояние:
    • Часть старого массива уже освобождена
    • Часть нового массива еще не инициализирована
  4. UICollectionView пытался обратиться к несуществующим индексам

Проблема усугублялась тем, что Swift-массивы имеют внутреннюю оптимизацию копирования при записи (Copy-on-Write), но в момент прямого присваивания в многопоточной среде возникало окно уязвимости.

Решение

class FeedCacheManager {
    static let shared = FeedCacheManager()
    private var cachedItems: [FeedItem] = []
    private let accessQueue = DispatchQueue(label: "com.app.feedcache", attributes: .concurrent)
    
    func updateItems(_ newItems: [FeedItem]) {
        accessQueue.async(flags: .barrier) {
            self.cachedItems = newItems
        }
    }
    
    func getItems() -> [FeedItem] {
        return accessQueue.sync {
            return self.cachedItems
        }
    }
}

Ключевые уроки

  1. Даже "простые" операции в Swift не атомарны без синхронизации
  2. SINGLETON-объекты с изменяемым состоянием требуют особой осторожности в конкурентных сценариях
  3. Thread Sanitizer и Address Sanitizer — незаменимые инструменты для выявления гонок данных
  4. Проектирование иммутабельных моделей данных часто предотвращает целый класс подобных проблем

Этот баг особенно интересен, потому что проявлялся только при специфическом тайминге операций, что делало его воспроизведение и диагностику сложной задачей. Он наглядно демонстрирует, как абстракции языка (типа Swift-массивов) могут "протекать" в многопоточном контексте, и почему понимание низкоуровневого поведения систем остается критически важным даже при работе с высокоуровневыми языками.

Приведи пример интересного бага | PrepBro