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

Расскажи про сложный баг с которым сталкивался

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

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

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

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

Сложный баг с race condition в многопоточной синхронизации

Один из наиболее сложных и поучительных багов, с которым мне пришлось столкнуться, был связан с race condition (состоянием гонки) в системе кэширования изображений в приложении социальной сети. Баг проявлялся случайным образом: примерно в 0.1% случаев при быстрой прокрутке ленты новостей приложение падало с ошибкой EXC_BAD_ACCESS или некорректно отображало изображения.

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

Приложение использовало кастомную систему кэширования изображений с несколькими уровнями:

  • Memory cache (NSCache) для быстрого доступа
  • Disk cache (файловая система) для персистентного хранения
  • Сетевые запросы для загрузки отсутствующих изображений

Основная проблема была в реализации асинхронной загрузки с многократными проверками наличия данных:

class ImageCacheManager {
    private let memoryCache = NSCache<NSString, UIImage>()
    private let ioQueue = DispatchQueue(label: "com.app.imageCacheIO", attributes: .concurrent)
    private var operations: [String: Operation] = [:]
    
    func loadImage(for url: URL, completion: @escaping (UIImage?) -> Void) {
        let key = url.absoluteString
        
        // Проверка в memory cache (главный поток)
        if let cachedImage = memoryCache.object(forKey: key as NSString) {
            completion(cachedImage)
            return
        }
        
        // Проверка в disk cache (фоновый поток)
        ioQueue.async { [weak self] in
            guard let self = self else { return }
            
            // ВТОРАЯ проверка в memory cache (ошибка!)
            if let cachedImage = self.memoryCache.object(forKey: key as NSString) {
                DispatchQueue.main.async {
                    completion(cachedImage)
                }
                return
            }
            
            // Проверка на диске
            if let diskImage = self.loadFromDisk(key: key) {
                self.memoryCache.setObject(diskImage, forKey: key as NSString)
                DispatchQueue.main.async {
                    completion(diskImage)
                }
                return
            }
            
            // Загрузка из сети
            self.downloadImage(url: url, key: key, completion: completion)
        }
    }
}

Анализ проблемы

Баг возникал из-за неатомарности операций и отсутствия proper locking mechanism. Конкретные проблемы:

  1. Double-check locking антипаттерн: Вторая проверка memoryCache в фоновом потоке выполнялась без синхронизации с главным потоком
  2. Race condition между потоками: Одна и та же картинка могла запрашиваться одновременно из нескольких ячеек таблицы
  3. Конкурентные операции записи: Несколько потоков могли одновременно пытаться сохранить одно изображение и в memory cache, и на диск
  4. Проблемы с жизненным циклом объектов: UIImage мог быть деаллоцирован в одном потоке, пока другой поток пытался его использовать

Отладка и диагностика

Диагностика была сложной, потому что:

  • Баг воспроизводился только на реальных устройствах при быстрой прокрутке
  • Инструменты типа Thread Sanitizer плохо работали с NSCache
  • В стектрейсах не было полезной информации, только краш в системных библиотеках

Использовал комбинацию методов:

  • Кастомные логи с идентификаторами потоков и временными метками
  • Assertions для проверки предположений о потокобезопасности
  • Stress testing с искусственным замедлением операций

Решение

Конечное решение включало несколько изменений:

class ImageCacheManager {
    private let memoryCache = NSCache<NSString, UIImage>()
    private let ioQueue = DispatchQueue(label: "com.app.imageCacheIO")
    private let lockQueue = DispatchQueue(label: "com.app.imageCacheLock", attributes: .concurrent)
    private var operations: [String: Operation] = [:]
    private var pendingCompletions: [String: [(UIImage?) -> Void]] = [:]
    
    func loadImage(for url: URL, completion: @escaping (UIImage?) -> Void) {
        let key = url.absoluteString
        
        // Быстрая проверка под lock
        var image: UIImage?
        lockQueue.sync {
            image = memoryCache.object(forKey: key as NSString)
        }
        
        if let cachedImage = image {
            completion(cachedImage)
            return
        }
        
        // Сериализация операций для одного ключа
        ioQueue.async { [weak self] in
            guard let self = self else { return }
            
            self.lockQueue.async(flags: .barrier) {
                // Добавление completion в очередь
                if self.pendingCompletions[key] == nil {
                    self.pendingCompletions[key] = []
                }
                self.pendingCompletions[key]?.append(completion)
                
                // Если операция уже выполняется, просто добавляем completion
                if self.pendingCompletions[key]?.count ?? 0 > 1 {
                    return
                }
            }
            
            // Загрузка изображения (выполняется только один раз для каждого ключа)
            if let diskImage = self.loadFromDisk(key: key) {
                self.cacheImage(diskImage, for: key)
            } else {
                self.downloadImage(url: url, key: key)
            }
        }
    }
    
    private func cacheImage(_ image: UIImage, for key: String) {
        lockQueue.async(flags: .barrier) {
            self.memoryCache.setObject(image, forKey: key as NSString)
            
            // Вызов всех ожидающих completion handlers
            if let completions = self.pendingCompletions[key] {
                DispatchQueue.main.async {
                    for completion in completions {
                        completion(image)
                    }
                }
                self.pendingCompletions.removeValue(forKey: key)
            }
        }
    }
}

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

  1. Многопоточность требует тщательного проектирования с самого начала
  2. NSCache не является потокобезопасным при конкурентном доступе на запись и чтение
  3. Double-check locking паттерн в Swift/Objective-C требует особой осторожности
  4. Важно минимизировать разделяемое mutable состояние между потоками
  5. Инструменты отладки (Thread Sanitizer, Address Sanitizer) должны использоваться proactively

Этот опыт научил меня важности написания потокобезопасного кода с первого дня и необходимости тщательного тестирования сценариев конкурентного доступа при работе с любыми системами кэширования.

Расскажи про сложный баг с которым сталкивался | PrepBro