Background Thread — потік виконання, не пов’язаний з інтерфейсом користувача, призначений для тривалих операцій: мережеві запити, робота з файлами, JSON-парсинг, стиснення зображень, шифрування та запити до бази даних. В iOS фонові потоки керуються через GCD (DispatchQueue.global) та OperationQueue; в Android — через Executors, WorkManager та Kotlin Coroutines (Dispatchers.IO, Dispatchers.Default). Згідно з Документацією Apple DispatchQueue, після завершення фонової операції результат необхідно повернути на Main Thread для оновлення інтерфейсу.
Головне
Background Thread — будь-який потік у додатку, який не є Main Thread і не має доступу до UI. Його завдання — звільнити головний потік від важких операцій, щоб інтерфейс залишався чуйним. Операційна система розподіляє фонові потоки по ядрах процесора, дозволяючи виконувати кілька завдань паралельно. iOS автоматично керує пулом потоків через GCD, Android — через пули Java Executors.
На відміну від Main Thread, який обробляє події послідовно (одна за одною), фонові потоки можуть виконуватися паралельно, обмежені лише кількістю ядер CPU. Наприклад, на пристрої з 8 ядрами можна запустити до 8 паралельних фонових завдань без значного уповільнення. Однак надмірна кількість потоків (сотні) призводить до thread starvation — конкуренції за ядра та зростання накладних витрат на перемикання контексту (context switch).
Quality of Service (QoS) — механізм iOS, який дозволяє вказати пріоритет фонового завдання. Значення: .userInteractive (найвищий, майже Main Thread), .userInitiated (користувач очікує результат), .default (стандартний), .utility (користувач не чекає безпосередньо), .background (найнижчий, для синхронізації та індексації). В Android аналог — Thread.setPriority() від 1 до 10, але Android також використовує cgroups для групового керування пріоритетами потоків.
DispatchQueue.global(qos:) — основний спосіб отримання фонової черги в iOS. GCD (Grand Central Dispatch) автоматично створює пул потоків і розподіляє завдання по ядрах. Виклик DispatchQueue.global(qos: .background).async {} надсилає блок виконання у фонову чергу з найнижчим пріоритетом. Для завдань, результат яких потрібен негайно, використовуйте .userInitiated або .utility.
OperationQueue — абстракція вищого рівня над GCD, яка дозволяє задавати залежності між операціями, максимальну кількість одночасно виконуваних операцій (maxConcurrentOperationCount) та пріоритети. OperationQueue зручна для складних багатозадачних ланцюжків: завантажити файл → розпакувати → зберегти в кеш. За замовчуванням OperationQueue використовує фонові потоки, якщо не вказано інше.
import UIKit
class ImageDownloader {
func downloadImagesSequentially() {
let urls = ["https://example.com/1.png", "https://example.com/2.png"]
// OperationQueue з maxConcurrentOperationCount = 2
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2
queue.qualityOfService = .utility
for urlString in urls {
queue.addOperation {
guard let url = URL(string: urlString),
let data = try? Data(contentsOf: url)
else { return }
DispatchQueue.main.async {
print("Загружено: \(url.lastPathComponent)")
}
}
}
}
// GCD: глобальна фонова черга з різними QoS
func backgroundTaskWithQoS() {
DispatchQueue.global(qos: .userInitiated).async {
// Високий пріоритет — користувач чекає на результат
let result = self.heavyComputation()
DispatchQueue.main.async {
self.showResult(result)
}
}
}
private func heavyComputation() -> String {
Thread.sleep(forTimeInterval: 2) // імітація роботи
return "Результат обчислень"
}
private func showResult(_ result: String) {
print("Result on Main: \(result)")
}
}
У прикладі OperationQueue завантажує два зображення паралельно (maxConcurrentOperationCount = 2) з фоновим QoS через qualityOfService = .utility. GCD-метод backgroundTaskWithQoS використовує глобальну чергу з .userInitiated для завдання, результат якого очікує користувач. Обидва підходи завершуються поверненням на DispatchQueue.main для оновлення UI — це обов’язкова вимога iOS.
GCD підтримує два типи черг: serial (послідовні) та concurrent (паралельні). Serial черги виконують завдання одне за одним — це зручно для доступу до спільного ресурсу (файл, БД) без блокувань. Concurrent черги виконують завдання паралельно, розподіляючи їх по вільних ядрах. DispatchQueue.global — завжди concurrent. Для створення serial черги використовуйте DispatchQueue(label: "com.app.queue").
Android надає кілька рівнів абстракції для фонових потоків. Класичний підхід — java.util.concurrent.Executors.newFixedThreadPool(n) або Executors.newCachedThreadPool(). Сучасний — Kotlin Coroutines з Dispatchers.IO (для введення-виведення: мережа, файли, БД) та Dispatchers.Default (для CPU-інтенсивних завдань: сортування, обробка зображень). WorkManager — для відкладених та гарантованих фонових завдань.
HandlerThread — спеціалізований клас Android для створення фонового потоку з власним Looper (чергою повідомлень). На відміну від Executors, HandlerThread дозволяє надсилати повідомлення та Runnable через Handler. Використовується для операцій, які потребують постановки в чергу (наприклад, послідовний запис у БД). Після використання потрібно викликати quit() або quitSafely() для звільнення ресурсів.
// Android: Executors та Dispatchers корутин
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.concurrent.Executors
class DataRepository {
private val ioExecutor = Executors.newFixedThreadPool(4)
// Класичний підхід через Executors
fun loadDataLegacy(callback: (String) -> Unit) {
ioExecutor.execute {
val result = readFromFile()
val handler = android.os.Handler(android.os.Looper.getMainLooper())
handler.post { callback(result) }
}
}
// Сучасний підхід через Coroutines
suspend fun loadDataCoroutines(): String {
return withContext(Dispatchers.IO) {
// Файлова операція — виконується в фоновому пулі
readFromFile()
}
// Результат автоматично повертається на Dispatchers.Main
}
// CPU-інтенсивне завдання на Dispatchers.Default
suspend fun processImage(pixels: IntArray): IntArray {
return withContext(Dispatchers.Default) {
// Сортування, фільтрація — виконується на Default-пулі
pixels.sortedArray()
}
}
private fun readFromFile(): String {
Thread.sleep(1000) // імітація читання з файлу
return "file_content"
}
fun cleanup() {
ioExecutor.shutdown()
}
}
Приклад DataRepository показує еволюцію фонових потоків в Android. Legacy-метод loadDataLegacy використовує Executors.newFixedThreadPool(4) з Handler для повернення на Main Thread. Сучасний loadDataCoroutines використовує withContext(Dispatchers.IO) — корутина призупиняється під час роботи, не блокуючи потік, та автоматично відновлюється на Main Thread. Dispatchers.Default рекомендовано для CPU-bound операцій (сортування, фільтрація, трансформація даних).
Kotlin Coroutines — не просто спосіб роботи з потоками, а принципово інша модель: асинхронні завдання не прив’язані до конкретного потоку і можуть призупинятися (suspend) без блокування. Це означає, що у фоні корутина не займає потік, а звільняє його для інших завдань. Механізм призупинення дозволяє виконувати сотні тисяч конкурентних завдань на пулі з 4–8 потоків без thread starvation.
Три основні диспетчери: Dispatchers.Main (UI, один потік), Dispatchers.IO (64 потоки за замовчуванням для блокуючих операцій: мережа, файли, БД), Dispatchers.Default (дорівнює кількості ядер CPU, для інтенсивних обчислень). Комбінуючи їх через withContext, розробник перемикається між потоками без створення callback-ів. withContext — це suspend-функція, яка не повертає керування, поки завдання не виконано.
// Корутини: композиція фонових завдань
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
suspend fun loadUserProfile(userId: String): UserProfile =
coroutineScope {
// Паралельне завантаження даних з різних джерел
val user = async(Dispatchers.IO) { fetchUser(userId) }
val posts = async(Dispatchers.IO) { fetchPosts(userId) }
val avatar = async(Dispatchers.Default) {
processAvatar(fetchAvatar(userId))
}
// await() — призупиняється до завершення всіх завдань
UserProfile(
user = user.await(),
posts = posts.await(),
avatar = avatar.await()
)
}
data class UserProfile(
val user: String,
val posts: List<String>,
val avatar: ByteArray
)
suspend fun fetchUser(id: String): String { delay(300); return "User:$id" }
suspend fun fetchPosts(id: String): List<String> { delay(500); return listOf("Post1") }
suspend fun fetchAvatar(id: String): ByteArray { delay(200); return ByteArray(1024) }
suspend fun processAvatar(data: ByteArray): ByteArray { delay(100); return data }
Функція loadUserProfile запускає три паралельні фонові завдання через async. fetchUser та fetchPosts — IO-bound (мережа), виконуються на Dispatchers.IO. processAvatar — CPU-bound (обробка зображення), виконується на Dispatchers.Default. await() призупиняє корутину до завершення всіх завдань. Загальний час виконання дорівнює максимальному часу серед трьох завдань (500 мс для fetchPosts), а не їхній сумі. Це ключова перевага корутин перед послідовним виконанням.
Structured concurrency — принцип, при якому кожна корутина має батьківський scope, і скасування батька автоматично скасовує дочірні корутини. В Android lifecycleScope скасовує всі корутини при знищенні Activity. viewModelScope — при очищенні ViewModel. Це запобігає витокам фонових завдань: якщо користувач закрив екран, у фоні корутина не продовжуватиме завантаження даних, які вже нікому не потрібні.
WorkManager — бібліотека Android Jetpack для виконання фонових завдань, які мають бути виконані навіть після перезапуску пристрою або закриття додатку. На відміну від Executors та корутин, які живуть у процесі додатку, WorkManager передає завдання диспетчеру системи, який гарантує виконання за відповідних умов (наявність мережі, заряд батареї, вільне місце). WorkManager підходить для синхронізації даних, завантаження логів, резервного копіювання.
Завдання в WorkManager — це клас, який успадковує Worker (або CoroutineWorker для корутин). Worker.doWork() виконується на фоновому потоці, наданому WorkManager. Результат повертається через Result.success(), Result.retry() або Result.failure(). Завдання можна об’єднувати в ланцюжки: oneTimeWorkRequest.andThen(nextRequest).enqueue(). WorkManager сам вибирає оптимальний час виконання з урахуванням обмежень (Constraints).
// WorkManager з корутинами
import android.content.Context
import androidx.work.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class SyncWorker(
appContext: Context,
workerParams: WorkerParameters
) : CoroutineWorker(appContext, workerParams) {
override suspend fun doWork(): Result {
// Виконується на Dispatchers.Default (за замовчуванням)
return withContext(Dispatchers.IO) {
try {
syncDataToServer()
Result.success()
} catch (e: Exception) {
if (runAttemptCount < 3) Result.retry() else Result.failure()
}
}
}
private suspend fun syncDataToServer() {
// Імітація синхронізації
delay(1000)
}
}
// Запуск WorkManager завдання з обмеженнями
fun scheduleSync(context: Context) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
val syncWork = OneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, java.util.concurrent.TimeUnit.SECONDS)
.build()
WorkManager.getInstance(context).enqueue(syncWork)
}
SyncWorker успадковує CoroutineWorker — версію Worker, яка підтримує корутини. doWork() виконується на Dispatchers.Default, перемикання на IO для мережевих операцій через withContext. Constraints гарантують, що синхронізація запуститься лише за наявності мережі та заряду батареї не нижче низького рівня. BackoffCriteria з EXPONENTIAL збільшує інтервал між повторними спробами: 10, 20, 40 секунд.
Для регулярних завдань (синхронізація кожні 15 хвилин, надсилання аналітики щогодини) WorkManager надає PeriodicWorkRequestBuilder. Мінімальний інтервал — 15 хвилин. На відміну від OneTimeWorkRequest, PeriodicWorkRequest не гарантує точне дотримання інтервалу — система може об’єднувати кілька періодичних завдань для економії батареї. Для точних інтервалів використовуйте AlarmManager, але враховуйте обмеження Android 12+ на точні будильники.
Перша помилка — створення нового Thread для кожного завдання. new Thread().start() створює нативний потік з виділенням ~1 МБ під стек. Для 100 паралельних завдань це 100 МБ лише на стеки, плюс накладні витрати на context switch. Використовуйте пули потоків: Executors.newFixedThreadPool(n) (Android) або DispatchQueue.global() (iOS) — вони перевикористовують потоки, знижуючи overhead у десятки разів.
Друга помилка — доступ до mutable стану з кількох фонових потоків без синхронізації. Якщо два фонові потоки одночасно пишуть в один і той самий ArrayList або HashMap, виникають race condition: ConcurrentModificationException в Android, data corruption в iOS. Рішення: використовуйте потокобезпечні колекції (ConcurrentHashMap, CopyOnWriteArrayList) або серіалізуйте доступ через одну чергу (DispatchQueue serial).
Третя помилка — фонові завдання без керування життєвим циклом. Запуск корутини в глобальному scope без прив’язки до життєвого циклу Activity або ViewModel призводить до витоків: завдання продовжує виконуватися після знищення екрану. В Android використовуйте lifecycleScope (Activity/Fragment) або viewModelScope (ViewModel). В iOS — weak self у замиканнях та скасування завдань при deinit.
Часті запитання
Background Thread — потік, на якому виконуються операції, не пов’язані з UI: мережеві запити, читання/запис файлів, JSON-парсинг, обчислення. Він звільняє Main Thread від важкої роботи, зберігаючи чуйність інтерфейсу. В iOS фонові потоки керуються через GCD (DispatchQueue.global), в Android — через Executors або Kotlin Coroutines (Dispatchers.IO, Dispatchers.Default).
Dispatchers.IO призначений для блокуючих операцій введення-виведення: читання файлів, мережеві запити, робота з БД. Він має пул із 64 потоків. Dispatchers.Default — для CPU-інтенсивних завдань: сортування, фільтрація, обробка зображень. Його пул дорівнює кількості ядер CPU. Використання Dispatchers.Default для IO-операцій може заблокувати всі ядра, а Dispatchers.IO для CPU-завдань — створити надмірну кількість потоків.
DispatchQueue.global(qos: .background).async { } надсилає блок виконання на глобальну фонову чергу. Після завершення фонової роботи потрібно повернутися на головний потік через DispatchQueue.main.async { } для оновлення UI. Для послідовних фонових завдань використовуйте OperationQueue з maxConcurrentOperationCount = 1 або DispatchQueue(label: "serial").
Рекомендована кількість фонових потоків дорівнює числу ядер CPU плюс 1 для IO-bound завдань. На сучасному 8-ядерному пристрої це 9 потоків. Створення сотень потоків призводить до thread starvation: ОС витрачає більше часу на перемикання контексту (context switch), ніж на виконання завдань. GCD в iOS та Executors в Android автоматично оптимізують пул потоків під поточний пристрій.
В Kotlin Coroutines повернення на Main Thread відбувається автоматично, якщо корутина запущена в Main-скоупі (lifecycleScope.launch, viewModelScope.launch). Функція withContext(Dispatchers.IO) призупиняє корутину на IO-потоці, а після завершення автоматично відновлює її на тому диспетчері, де була запущена (зазвичай Main). Явного виклику DispatchQueue.main.async не потрібно.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також