Каждое мобильное приложение выполняет множество задач одновременно: загружает данные из сети, обрабатывает касания пользователя, анимирует интерфейс и сохраняет файлы. Если весь этот код работает в одном потоке, приложение зависает при любой сетевой задержке. Многопоточность (multithreading) и concurrency — ключевые концепции, позволяющие приложению оставаться отзывчивым и эффективным. В этой статье разберём все основные инструменты: от Main Thread и RunLoop до корутин Kotlin и Combine на iOS. Материал основан на официальной документации Apple GCD.
Главное
Многопоточность — это способность приложения выполнять несколько фрагментов кода одновременно. Каждый фрагмент выполняется в отдельном потоке (Thread) — лёгком процессе с собственным стеком вызовов. В мобильной разработке потоки делятся на две категории: Main Thread (UI-поток) и Background Threads (фоновые потоки).
Операционная система сама управляет распределением потоков по ядрам процессора. На современных устройствах может быть 6–8 ядер, поэтому параллельное выполнение реально ускоряет работу. Однако создание потоков — дорогая операция, поэтому напрямую работать с Thread не рекомендуется. Вместо этого используются абстракции более высокого уровня: DispatchQueue, OperationQueue, CoroutineDispatcher.
Concurrency (конкурентность) — более широкое понятие, чем многопоточность. Concurrency означает, что задачи могут выполняться «одновременно» даже на одном ядре за счёт переключения контекста. Асинхронность (Async/Await) — модель программирования, при которой задача не блокирует поток, а возвращает управление, пока ожидается результат. Современные языки (Kotlin, Swift, Dart) имеют встроенную поддержку Async/Await.
В IT Sectr мы уделяем особое внимание правильной архитектуре многопоточности на старте проекта. Ошибки, заложенные в ранней стадии, приводят к трудноотлавливаемым багам: гонкам данных, взаимным блокировкам и нестабильности приложения под нагрузкой. Каждый наш проект проходит ревью архитектуры concurrency на этапе планирования.
Main Thread (главный поток) — единственный поток в мобильном приложении, который имеет доступ к UI. На Android это называется UI Thread, на iOS — Main Thread. Все операции с интерфейсом — изменение текста, анимация, обработка касаний — выполняются только на Main Thread. Если на главном потоке выполняется тяжёлая операция (загрузка файла, парсинг JSON), интерфейс перестаёт реагировать. На Android это приводит к ANR (Application Not Responding), на iOS — к «заморозке» экрана.
Background Threads (фоновые потоки) предназначены для всего, что не касается UI: сетевые запросы, работа с базой данных, обработка изображений, криптография. После завершения работы результат передаётся на Main Thread для отображения. Каждая платформа предоставляет свои инструменты для переключения между потоками: DispatchQueue.main.async в iOS, runOnUiThread или withContext(Dispatchers.Main) в Android.
RunLoop — цикл обработки событий на главном потоке iOS. RunLoop ожидает события (касания, таймеры, уведомления) и диспетчеризует их в соответствующие обработчики. На Android аналог — Looper, связанный с каждым Main Thread. Main Looper бесконечно извлекает сообщения из очереди и передаёт их Handler для обработки. Понимание RunLoop и Looper помогает избежать утечек памяти и «залипания» интерфейса.
Grand Central Dispatch (GCD) — библиотека Apple для управления многопоточностью на уровне языка C. GCD работает с DispatchQueue — очередями задач. Разработчик не создаёт потоки вручную; GCD управляет пулом потоков (Thread Pool), распределяя задачи по доступным ядрам процессора. DispatchQueue бывают двух типов: Serial Queue (последовательная — задачи выполняются одна за другой) и Concurrent Queue (параллельная — задачи могут выполняться одновременно).
Main DispatchQueue — это последовательная очередь, привязанная к главному потоку. Global Queues — параллельные очереди с разными приоритетами (QoS — Quality of Service): userInteractive, userInitiated, utility, background. Выбор правильного QoS критичен для производительности: .userInteractive — для задач, влияющих на UI (анимации, рендеринг); .background — для задач, не критичных ко времени (синхронизация, очистка кеша).
OperationQueue — надстройка над GCD с дополнительными возможностями: отмена задач, установка зависимостей между операциями, контроль максимального количества параллельных операций. Операции — объекты класса Operation (или BlockOperation). Пример: если нужно загрузить изображение, затем применить фильтр и только потом показать его — OperationQueue с зависимостями справится идеально. В GCD пришлось бы вручную синхронизировать эти шаги через DispatchGroup или semaphore.
Async/Await в Swift 5.5+ — современная альтернатива GCD. Ключевые слова async и await делают асинхронный код линейным и читаемым. Функции помечаются как async, а вызов ожидается через await. Система сама управляет переключением контекста: по умолчанию async-функция выполняется на фоновом потоке, а обновление UI — на MainActor. @MainActor — атрибут, гарантирующий выполнение кода на главном потоке.
Coroutines (корутины) — это легковесные потоки для Kotlin, разработанные JetBrains. В отличие от обычных потоков, корутины не привязаны к конкретному Thread. Тысячи корутин могут выполняться на нескольких потоках без значительных накладных расходов. CoroutineScope управляет жизненным циклом корутин: viewModelScope привязан к ViewModel, lifecycleScope — к Activity/Fragment. При уничтожении скоупа все дочерние корутины автоматически отменяются.
Dispatchers определяют, на каком пуле потоков выполняется корутина: Dispatchers.Main — UI-поток; Dispatchers.IO — для сетевых запросов и дисковых операций; Dispatchers.Default — для CPU-ёмких вычислений. Для переключения диспетчера используется withContext. Корутины поддерживают结构化并发 (structured concurrency): каждая корутина имеет родителя, и при отмене родителя отменяются все дочерние корутины. Это предотвращает утечки памяти и висящие задачи.
Flow — холодный асинхронный поток данных из библиотеки корутин. Flow эмитирует значения последовательно: (1) продюсер генерирует данные, (2) операторы трансформируют поток, (3) коллектор потребляет результат. В отличие от LiveData, Flow поддерживает сложные цепочки операторов (map, filter, flatMapConcat, catch) и полностью потоко-безопасен. StateFlow и SharedFlow — горячие разновидности Flow, идеально подходящие для состояния UI и одноразовых событий (Snackbar, навигация).
Channel — ещё одна абстракция корутин для передачи данных между корутинами. Channel работает как очередь: один отправитель (send) и один или несколько получателей (receive). Буферизированные каналы (Channel(UNLIMITED), Channel(BUFFERED)) позволяют настроить поведение при переполнении. Channel часто используют в паре с Flow для bridging callback-based API в корутины: callbackFlow { … }.
В IT Sectr мы активно используем корутины и Flow во всех Android-проектах. Это позволяет писать асинхронный код, который выглядит как синхронный, легко тестируется (runTest, TestDispatcher) и не требует ручного управления потоками. Пример простой корутины с загрузкой данных:
class UserRepository(
private val api: UserApi,
private val dao: UserDao
) {
suspend fun getUsers(): List<User> = withContext(Dispatchers.IO) {
return@withContext try {
val users = api.fetchUsers()
dao.insertAll(users)
users
} catch (e: Exception) {
dao.getAll()
}
}
}
Реактивное программирование — парадигма, в которой данные распространяются в виде асинхронных потоков (Observable, Publisher). RxJava/RxKotlin — самая популярная реализация для Android, портированная из .NET Rx. RxSwift — аналогичная библиотека для iOS. Основные компоненты: Observable (источник событий), Observer (подписчик), Scheduler (управление потоками), Operators (трансформация потока).
Combine — фреймворк Apple для реактивного программирования, представленный в iOS 13. Combine использует протоколы Publisher (издатель) и Subscriber (подписчик). В отличие от RxSwift, Combine встроен в SDK и тесно интегрирован с SwiftUI. Операторы в Combine: map, filter, combineLatest, zip, debounce, throttle — покрывают большинство сценариев: от привязки данных к UI до дебаунса поискового запроса.
Future и Promise — паттерны для работы с единственным асинхронным результатом. Future представляет значение, которое будет доступно позже. Promise — обещание предоставить значение. В Rx это Single (один успешный ответ или ошибка), в Combine — Future Publisher. На практике Future/Promise удобны для единичных запросов к API, а Observable/Publisher — для непрерывных потоков (геолокация, ввод текста).
Callback и Delegate — классические паттерны для асинхронных операций. Callback — функция, переданная как аргумент и вызываемая при завершении операции. Delegate — объект, реализующий протокол с методами-обработчиками событий. Недостаток: «callback hell» (вложенные колбэки) и сложность обработки ошибок. NotificationCenter (iOS) и EventBus (Android) — механизмы широковещательной рассылки событий, полезные для слабосвязанной коммуникации, но ведущие к неявным зависимостям.
Многопоточность открывает доступ к высокой производительности, но одновременно создаёт риск трудноуловимых ошибок. Самые распространённые: Race Condition (состояние гонки), Deadlock (взаимная блокировка), Livelock (активная блокировка) и Starvation (голодание потока). Понимание этих проблем — обязательный навык любого мобильного разработчика.
Race Condition возникает, когда два или более потока одновременно читают и записывают одни и те же данные без синхронизации. Результат зависит от того, какой поток выполнится первым. Классический пример: два потока инкрементируют счётчик. Операция «прочитать → увеличить → записать» не атомарна, поэтому при одновременном выполнении один инкремент «теряется». Решение — использовать атомарные операции (AtomicInteger, AtomicReference) или блокировки (Mutex, Semaphore, synchronized).
Deadlock — ситуация, когда каждый поток удерживает ресурс и ожидает ресурс, захваченный другим потоком. Ни один поток не может продолжить выполнение. Условия возникновения: взаимное исключение, удержание и ожидание, отсутствие вытеснения, циклическое ожидание. Предотвращение: установить единый порядок захвата блокировок, использовать tryLock с таймаутом, применять Lock-Free алгоритмы (ConcurrentHashMap, CopyOnWriteArrayList).
Livelock — потоки не заблокированы, но постоянно «перекладывают» ресурсы друг другу, не выполняя полезную работу. Пример: два человека встречаются в коридоре и оба уступают дорогу, двигаясь в одну и ту же сторону. Starvation — поток не получает доступа к ресурсу, потому что другие потоки постоянно его перехватывают. Решение: справедливые блокировки (fair locks), приоритеты потоков с осторожностью.
Для предотвращения проблем многопоточности используются примитивы синхронизации: Mutex (взаимное исключение), Semaphore (ограничение количества одновременных доступов), Lock (интерфейс с tryLock), Synchronized (блокировка уровня JVM), @MainActor (Swift — гарантия выполнения на главном потоке). На Android также доступны ThreadPool (пул потоков) через Executors.newFixedThreadPool, newCachedThreadPool. Однако ручное управление пулами — прерогатива легаси-проектов; в новых проектах лучше использовать корутины.
| Инструмент | Платформа | Тип | Особенности |
|---|---|---|---|
| DispatchQueue (GCD) | iOS | Очередь задач | Serial/Concurrent, QoS приоритеты, Thread Pool управляется системой |
| OperationQueue | iOS | Очередь операций | Зависимости, отмена, maxConcurrentOperationCount |
| Coroutines + Flow | Android | Корутины | Лёгкие, structured concurrency, StateFlow, Channel |
| RxJava / RxKotlin | Android | Реактивный поток | Observable, Schedulers, богатый набор операторов |
| Combine | iOS | Реактивный поток | Publisher/Subscriber, интеграция со SwiftUI |
| Async/Await + Task | iOS / Android | Асинхронная модель | Линейный код, @MainActor, structured concurrency |
Часто задаваемые вопросы
Main Thread (UI-поток) отвечает за отрисовку интерфейса и обработку касаний. Background Thread выполняет фоновые задачи — загрузку данных, вычисления, работу с сетью. Блокировка Main Thread вызывает зависание интерфейса (ANR на Android, frozen UI на iOS).
Race Condition — состояние гонки, когда два потока одновременно обращаются к общим данным и результат зависит от порядка выполнения. Избегается с помощью синхронизации: Mutex, Semaphore, Lock, Synchronized, @MainActor или атомарных операций.
Coroutines — современный стандарт для Android (JetBrains, поддерживается Google). RxJava/RxKotlin — реактивный подход с богатым набором операторов. Coroutines проще для асинхронных вызовов, RxJava мощнее для сложных потоков данных. В IT Sectr мы используем Coroutines + Flow для новых проектов.
Deadlock — взаимная блокировка, когда два потока ждут освобождения ресурсов друг друга. Livelock — потоки не заблокированы, но постоянно перекладывают ресурсы, не выполняя полезную работу. Обе проблемы решаются правильным порядком захвата блокировок и таймаутами.
DispatchQueue — абстракция Grand Central Dispatch (GCD) для управления потоками. Main Queue выполняет задачи на главном потоке, Global Queues — на фоновых. Serial Queue гарантирует последовательное выполнение, Concurrent Queue — параллельное. В современных проектах GCD часто заменяют Async/Await и Task.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.