Оптимізація потоків та конкурентності мобільної програми
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() (no-op у iOS 12+), перевели збереження кошика на 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 днів залежно від глибини архітектурних рішень.







