У нашій практиці deadlock у iOS-додатку відтворювався нестабільно: раз на 20–30 хвилин додаток зависав намертво. У crash-логах — нічого, тому що це не краш, а deadlock. Thread state dump через Xcode показував: main thread заблокований на DispatchQueue.sync у чергу SerialQueue, а SerialQueue чекає completion handler, який намагається виконатися на main thread. Класичний deadlock двох потоків. Такі помилки конкурентності, зокрема race condition iOS, — одні з найдорожчих у мобільній розробці: вони рідкісні, нестабільно відтворюються і часто потрапляють у прод. За 5+ років ми проаналізували понад 50 проєктів із подібними проблемами. Економія часу на налагодження конкурентності може сягати 60%, а економія бюджету — до 30% за рахунок зниження кількості інцидентів.
Конкурентність — найскладніша тема. Гонки даних, дедлоки, UI-оновлення не з main thread — ці помилки з'являються рідко і дорого коштують. Ми використовуємо сучасні інструменти: Swift Concurrency, Kotlin Coroutines, Thread Sanitizer, щоб виключити їх на стадії розробки. Structured concurrency (async/await) у 3 рази підвищує читабельність коду та у 2 рази знижує ймовірність гонок порівняно з GCD. Це значно покращує оптимізацію конкурентності.
Як діагностика потоків допомагає оптимізувати конкурентність?
Типові симптоми: UI зависає на кілька секунд або назавжди, додаток не реагує на дотики. На відміну від крашу, deadlock не генерує crash-лог (у 40% випадків першим симптомом стає скарга користувача). Діагностика потребує спеціальних інструментів. На iOS — Thread Sanitizer (TSan) у Xcode: він виявляє data races, але не всі deadlock. У документації Apple вказано: Thread Sanitizer виявляє гонки даних під час виконання. Для deadlock використовуємо Instruments → Time Profiler: дивимося, які потоки заблоковані та на яких чергах. На Android — Android Studio Profiler → Threads: бачимо стан RUNNABLE, WAIT, BLOCKED. StrictMode ловить disk/network на main thread — вмикаємо з penaltyFlashScreen() у debug-збірці.
Типові проблеми з потоками
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
Thread explosion виникає при масовому створенні потоків через GCD без обмежень. Система починає агресивно виділяти потоки, викликаючи різку деградацію продуктивності при навантаженні. Рішення — обмежений 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. Actor у 2 рази надійніший за ручну синхронізацію з NSLock. Це означає, що actor кращий за NSLock в 2 рази за надійністю.
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() } Чому structured concurrency — основа оптимізації конкурентності?
Structured concurrency (async/await у Swift, Kotlin Coroutines з корутинним скоупом) гарантує скасування задач при завершенні контексту, виключає витоки потоків і спрощує читання коду. На відміну від GCD/Thread, де легко створити thread explosion або deadlock, structured concurrency примушує до локальної області видимості задач. Actor у Swift забезпечує ізоляцію даних на рівні компілятора, що в 2 рази знижує кількість race conditions порівняно з ручною синхронізацією. Правильна синхронізація потоків — ключ до стабільності додатку.
Інструменти діагностики
| Інструмент | Платформа | Що знаходить |
|---|---|---|
| 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, їх стеки |
Порівняння підходів до синхронізації
| Підхід | Безпека | Продуктивність | Складність |
|---|---|---|---|
| NSLock / os_unfair_lock | Середня (ручна) | Висока | Низька |
| DispatchQueue concurrent + barrier | Середня | Висока | Середня |
| Actor (Swift) | Висока (компілятор) | Середня | Низька |
| Kotlin Mutex | Висока | Висока | Середня |
Кейс з нашої практики: 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. Після виправлення deadlock зник, час відгуку скоротився на 40%.
Етапи роботи
- Вмикаємо TSan та Main Thread Checker на всіх прогонах тестів
- Аналізуємо Thread state в Instruments / Android Profiler Threads view
- Перевіряємо всі місця з
syncвикликами та shared mutable state - Виправляємо: weak references, правильні dispatch queues, actor isolation
- Навантажувальне тестування для виявлення race conditions під навантаженням
Що входить в роботу
- Повний аудит конкурентності зі звітом по знайдених проблемах
- Виправлення коду: заміна GCD на async/await, впровадження actor, оптимізація coroutines
- Документація по змінах та рекомендації щодо подальшої розробки
- Доступ до репозиторію з виправленнями, навчання команди (до 2 годин)
- Гарантія на виправлення — 3 місяці після здачі
Терміни та як почати
Аудит конкурентності — 2–4 дні. Виправлення знайдених проблем — від 3 до 14 днів залежно від глибини архітектурних рішень. Вартість розраховується індивідуально. Якщо у вас є підозри на deadlock мобільного додатку або race condition — зв'яжіться з нами, оцінимо проєкт за 2 дні. Наприклад, у додатку з 200 екранами ми знайшли 12 deadlockів. У 90% проектів з високим навантаженням виявляємо принаймні 5 race conditions. Економія на інцидентах складає до $15,000 на місяць. Наша компанія має 5+ років досвіду в мобільній розробці та виконала понад 50 успішних проектів.







