Расскажи про сложный баг с которым сталкивался
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Сложный баг с 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. Конкретные проблемы:
- Double-check locking антипаттерн: Вторая проверка
memoryCacheв фоновом потоке выполнялась без синхронизации с главным потоком - Race condition между потоками: Одна и та же картинка могла запрашиваться одновременно из нескольких ячеек таблицы
- Конкурентные операции записи: Несколько потоков могли одновременно пытаться сохранить одно изображение и в memory cache, и на диск
- Проблемы с жизненным циклом объектов:
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)
}
}
}
}
Ключевые уроки
- Многопоточность требует тщательного проектирования с самого начала
- NSCache не является потокобезопасным при конкурентном доступе на запись и чтение
- Double-check locking паттерн в Swift/Objective-C требует особой осторожности
- Важно минимизировать разделяемое mutable состояние между потоками
- Инструменты отладки (Thread Sanitizer, Address Sanitizer) должны использоваться proactively
Этот опыт научил меня важности написания потокобезопасного кода с первого дня и необходимости тщательного тестирования сценариев конкурентного доступа при работе с любыми системами кэширования.