Кожен мобільний додаток виконує безліч завдань одночасно: завантажує дані з мережі, обробляє дотики користувача, анімує інтерфейс та зберігає файли. Якщо весь цей код працює в одному потоці, додаток зависає при будь-якій мережевій затримці. Багатопотоковість (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 або семафор.
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 для bridg-інгу 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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.