Оптимизация потоков и конкурентности мобильного приложения
Deadlock в iOS-приложении воспроизводится нестабильно: раз в 20–30 минут приложение зависает намертво. В crash-логах — нет ничего, потому что это не краш, это deadlock. Thread state dump через Xcode показывает: main thread заблокирован на DispatchQueue.sync в очередь SerialQueue, а SerialQueue ждёт completion handler, который пытается выполниться на main thread. Классический deadlock двух потоков.
Конкурентность — одна из сложнейших тем в мобильной разработке. Гонки данных, дедлоки, UI-обновления не с main thread — эти ошибки появляются редко, воспроизводятся нестабильно и дорого стоят в продакшене.
Типичные проблемы с потоками
UI-обновления не с main thread
На Android: CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. Причина — обработка сетевого ответа напрямую в колбэке Retrofit без withContext(Dispatchers.Main).
На iOS: Main Thread Checker в Xcode (включён по умолчанию в Scheme settings) ловит обращения к UIKit с фоновых потоков в debug-сборке. В релизе — случайные краши или visual corruption.
Правильный паттерн iOS:
DispatchQueue.global(qos: .userInitiated).async {
let result = heavyComputation()
DispatchQueue.main.async {
self.label.text = result // только здесь
}
}
Thread explosion с GCD
DispatchQueue.global().async создаёт новый поток при каждом вызове если все worker threads заняты. При 64+ одновременных async-задачах система начинает создавать потоки агрессивно — это thread explosion. Симптом: всё работает нормально, потом резкая деградация производительности при нагрузке.
Решение: ограниченный concurrency через OperationQueue.maxConcurrentOperationCount или через Swift Concurrency TaskGroup с явным withTaskGroup и ограниченным параллелизмом:
await withTaskGroup(of: Result.self) { group in
for item in items.prefix(4) { // не более 4 параллельных задач
group.addTask { await process(item) }
}
}
Data races
Несколько потоков читают и пишут одно поле без синхронизации. На Swift — Thread Sanitizer (TSan) находит гонки данных в debug-сборке. Включается в Scheme → Diagnostics → Thread Sanitizer.
Варианты синхронизации:
-
NSLock/os_unfair_lock— быстрые мьютексы для критических секций -
DispatchQueue(label:attributes:.concurrent)сbarrierдля read-write lock паттерна -
actorв Swift 5.5+ — самый современный способ, компилятор гарантирует изоляцию данных
actor UserCache {
private var storage: [String: User] = [:]
func get(_ id: String) -> User? { storage[id] }
func set(_ user: User) { storage[user.id] = user }
}
С actor компилятор не позволит обратиться к storage вне actor-контекста без await.
Android: неправильное использование Coroutines
GlobalScope.launch — красный флаг. Coroutine живёт бесконечно, не отменяется при закрытии экрана. При повторном открытии — создаётся второй. Правильно — viewModelScope.launch (отменяется при onCleared) или lifecycleScope.launch (отменяется при onDestroy).
Dispatchers.Main vs Dispatchers.Main.immediate: при вызове с main thread Dispatchers.Main.immediate выполняется синхронно без переключения контекста — важно для анимаций и немедленных UI-обновлений.
Неправильная обработка исключений в coroutines:
// НЕПРАВИЛЬНО — исключение не поймается
scope.launch {
try { riskyOperation() } catch (e: Exception) { handle(e) }
}
// ПРАВИЛЬНО — CoroutineExceptionHandler для структурной обработки
val handler = CoroutineExceptionHandler { _, e -> handleError(e) }
scope.launch(handler) { riskyOperation() }
Инструменты диагностики
| Инструмент | Платформа | Что находит |
|---|---|---|
| Thread Sanitizer (TSan) | iOS / Android | Data races |
| Main Thread Checker | iOS | UI из фонового потока |
| Instruments → Time Profiler | iOS | Заблокированные потоки |
| Android Studio Profiler → Threads | Android | Состояния потоков, sleep/block/run |
| StrictMode | Android | Disk/network на main thread |
| Kotlin Coroutines Debugger | Android | Активные coroutines, их стеки |
StrictMode на Android — включаем в debug-сборке:
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads().detectNetworkOnMainThread()
.penaltyLog().penaltyFlashScreen()
.build()
)
Мигание экрана при нарушении — невозможно проигнорировать.
Кейс: deadlock в Swift
E-commerce приложение: при добавлении в корзину UI иногда зависал на 30–60 секунд. Воспроизводилось только при плохом интернете.
Через Thread State dump выяснилось: CartService.addItem() вызывал userDefaults.synchronize() внутри serialQueue.sync, а synchronize() внутри ждал NSFileCoordinator, который тоже стоял в очереди на запись. При сетевой задержке несколько вызовов addItem() выстраивались в очередь и один из них попадал в deadlock с NSFileCoordinator.
Решение: убрали synchronize() (в iOS 12+ он no-op), перевели сохранение корзины на async запись через DispatchQueue.global().async.
Этапы работы
- Включаем TSan и Main Thread Checker на всех прогонах тестов
- Анализируем Thread state в Instruments / Android Profiler Threads view
- Проверяем все места с
syncвызовами и shared mutable state - Правим: weak references, правильные dispatch queues, actor isolation
- Нагрузочное тестирование для выявления race conditions под нагрузкой
Сроки
Аудит конкурентности — 2–4 дня. Исправление найденных проблем — 3–14 дней в зависимости от глубины архитектурных решений.







