В нашей практике deadlock в iOS-приложении воспроизводился нестабильно: раз в 20–30 минут приложение зависало намертво. В crash-логах — ничего, потому что это не краш, а deadlock. Thread state dump через Xcode показывал: main thread заблокирован на DispatchQueue.sync в очередь SerialQueue, а SerialQueue ждёт completion handler, который пытается выполниться на main thread. Классический deadlock двух потоков. Такие ошибки конкурентности — одни из самых дорогих в мобильной разработке: они редкие, нестабильно воспроизводятся и часто проходят в прод. За 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.
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 дня. Закажите аудит конкурентности сегодня и получите детальный отчёт с предложениями по исправлению.







