Thread Pool — це механізм керування потоками, за якого попередньо створений пул потоків перевикористовується для виконання завдань, уникаючи накладних витрат на створення та знищення потоків. У мобільній розробці thread pool використовується для фонових операцій: мережевих запитів, обробки зображень, роботи з базами даних. За даними Google Android Documentation (2025), ExecutorService є рекомендованим способом керування фоновими потоками в Android. В iOS аналогічну роль виконують OperationQueue та GCD DispatchQueue з глобальними concurrent чергами.
Головне
Thread Pool (пул потоків) — це архітектурний патерн, за якого фіксована кількість потоків створюється заздалегідь і перевикористовується для виконання багатьох завдань. Замість створення нового потоку на кожну операцію (що дорого: близько 1 МБ стеку на потік у JVM), завдання поміщаються в чергу та виконуються вільними потоками з пулу. У мобільній розробці thread pool критично важливий для продуктивності — Android та iOS обмежують кількість потоків на застосунок.
Створення потоку — дорога операція: виділення стеку, реєстрація в системі, контекстне перемикання. На мобільних пристроях з обмеженими ресурсами неконтрольоване створення потоків призводить до OOM (OutOfMemoryError) на Android та до троттлінгу на iOS. Thread Pool вирішує обидві проблеми: обмежує максимальну кількість одночасно працюючих потоків і перевикористовує вже створені. Google рекомендує ExecutorService замість raw Thread(), Apple рекомендує OperationQueue замість Thread.
| Параметр | Без пулу (raw Thread) | З Thread Pool |
|---|---|---|
| Створення потоку | На кожне завдання | Один раз при створенні пулу |
| Максимум потоків | Не обмежений (ризик OOM) | Обмежений core/max pool size |
| Утилізація | Низька (потік вмирає після завдання) | Висока (потік перевикористовується) |
| Керування | Ручне (join, interrupt) | Автоматичне (ExecutorService) |
| Споживання пам'яті | Зростає з кожним завданням | Фіксоване |
Thread pool працює за принципом Producer-Consumer: завдання (Runnable/Callable) поміщаються в блокуючу чергу (BlockingQueue). Потоки з пулу очікують завдання в черзі та забирають їх для виконання. Алгоритм: якщо вільних потоків менше corePoolSize, створюється новий потік. Якщо corePoolSize досягнуто, завдання поміщається в чергу. Якщо черга заповнена і потоків менше maximumPoolSize, створюється додатковий потік. При перевищенні maximumPoolSize завдання відхиляється через RejectedExecutionHandler.
Core pool size — кількість потоків, які підтримуються в пулі навіть у простої. Maximum pool size — максимальна кількість потоків, яка може бути створена при переповненні черги. Різниця між ними — це додаткові (overflow) потоки, які створюються тимчасово і завершуються після idle-таймауту. На мобільних пристроях рекомендується встановлювати corePoolSize рівним maximumPoolSize для уникнення пікових навантажень від створення потоків.
BlockingQueue зберігає завдання, що очікують виконання. Найпопулярніші реалізації: LinkedBlockingQueue (необмежена), ArrayBlockingQueue (обмежена) та SynchronousQueue (без зберігання — завдання одразу передається потоку). При переповненні черги та пулу спрацьовує RejectedExecutionHandler. Стандартні політики: AbortPolicy (кидає RejectedExecutionException), CallerRunsPolicy (виконує в потоці відправника), DiscardPolicy та DiscardOldestPolicy.
// Створення Thread Pool в Android
val threadPool = ThreadPoolExecutor(
corePoolSize = 2, // Мінімум 2 потоки
maximumPoolSize = 4, // Максимум 4 потоки
keepAliveTime = 30L, // Час життя overflow-потоку
unit = TimeUnit.SECONDS,
workQueue = LinkedBlockingQueue<Runnable>(16),
threadFactory = Executors.defaultThreadFactory(),
handler = ThreadPoolExecutor.CallerRunsPolicy()
)
// Відправка завдань
threadPool.execute {
val result = api.fetchData()
runOnUiThread { showData(result) }
}
// Завершення пулу
threadPool.shutdown()
// Очікування завершення всіх завдань
threadPool.awaitTermination(10, TimeUnit.SECONDS)
Android надає кілька реалізацій thread pool через java.util.concurrent. Executors — фабрика з готовими конфігураціями: newFixedThreadPool(n) (фіксований пул), newCachedThreadPool() (необмежений, потоки створюються за потреби), newSingleThreadExecutor() (один потік — послідовне виконання). Для мобільних проєктів рекомендується newFixedThreadPool з розумним лімітом (2-4 потоки), оскільки cached пул може створити занадто багато потоків.
ThreadPoolExecutor (TPE) — повна реалізація ExecutorService з налаштовуваними параметрами. На Android TPE використовується всередині AsyncTask, IntentService та JobIntentService. Параметри corePoolSize, maximumPoolSize, keepAliveTime, BlockingQueue та RejectedExecutionHandler дозволяють тонко налаштувати поведінку пулу. Рекомендації для Android: corePoolSize = кількість ядер CPU - 1 (для IO-bound завдань) або кількість ядер (для CPU-bound завдань). Для типових застосунків — 2-4 потоки.
// Готові конфігурації Executors
// 1. Фіксований пул на 3 потоки
val fixedPool = Executors.newFixedThreadPool(3)
// 2. Кешований пул (не рекомендується для мобільних)
val cachedPool = Executors.newCachedThreadPool()
// 3. Одиночний потік (серіалізація)
val singlePool = Executors.newSingleThreadExecutor()
// 4. Планувальник (періодичні завдання)
val scheduler = Executors.newScheduledThreadPool(2)
// Використання з Callable та Future
val future: Future<String> = fixedPool.submit(Callable {
"Result: ${api.call()}"
})
// Отримання результату (блокує потік)
val result = future.get(5, TimeUnit.SECONDS)
// Завершення пулу
fixedPool.shutdownNow()
Kotlin корутини надають CoroutineDispatcher — абстракцію, аналогічну thread pool. Dispatchers.IO використовує пул з 64 потоків (обмежених). Dispatchers.Default — пул, рівний кількості ядер CPU. CoroutineDispatcher не потребує ручного shutdown і керується автоматично. Для тонкого налаштування створіть власний ExecutorCoroutineDispatcher через Executors.newFixedThreadPool(2).asCoroutineDispatcher(). Корутини не замінюють thread pool, а обгортають його.
iOS надає два основні механізми для керування thread pool: OperationQueue (високорівневий API на основі GCD) та GCD DispatchQueue (низькорівневий C-API). OperationQueue інкапсулює thread pool через властивість maxConcurrentOperationCount. За замовчуванням OperationQueue використовує system-defined maximum (залежить від завантаження системи). DispatchQueue.global() надає concurrent чергу з системним пулом потоків.
OperationQueue керує пулом потоків через maxConcurrentOperationCount. Значення 1 створює serial чергу (аналог single thread pool). Значення більше 1 — concurrent пул із зазначеним лімітом. За замовчуванням maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount (системний оптимум, зазвичай 4-8 потоків). Operation підтримує залежності, пріоритети та скасування. Кожна операція виконується на будь-якому вільному потоці з системного пулу.
// OperationQueue з пулом на 3 потоки
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 3
queue.qualityOfService = .utility
// Створення операцій
let operation1 = BlockOperation {
let data = fetchData(from: url1)
DispatchQueue.main.async { updateUI(data) }
}
let operation2 = BlockOperation {
let data = fetchData(from: url2)
DispatchQueue.main.async { updateUI(data) }
}
// Залежність: operation2 чекає на operation1
operation2.addDependency(operation1)
// Додавання в чергу
queue.addOperations([operation1, operation2], waitUntilFinished: false)
// Скасування всіх операцій
queue.cancelAllOperations()
DispatchQueue — це thread pool від Apple. Concurrent черга (qos: .utility) використовує системний пул потоків, оптимізований для поточного завантаження пристрою. Різні QoS (userInteractive, userInitiated, utility, background) відображаються на різні пули з різним пріоритетом. DispatchGroup дозволяє синхронізувати кілька завдань. DispatchWorkItem підтримує скасування та qualityOfService. Для тонкого контролю створюйте власні concurrent черги через DispatchQueue(label: qos: attributes: .concurrent).
// GCD DispatchQueue як thread pool
let customQueue = DispatchQueue(
label: "com.app.background",
qos: .utility,
attributes: .concurrent,
autoreleaseFrequency: .workItem
)
// Відправка завдань у пул
customQueue.async { self.processFile(file1) }
customQueue.async { self.processFile(file2) }
// DispatchGroup для синхронізації
let group = DispatchGroup()
let pool = DispatchQueue.global(qos: .utility)
pool.async(group: group) { fetchData() }
pool.async(group: group) { processImage() }
group.notify(queue: .main) {
self.showResult() // Обидва завдання завершено
}
// Обмеження concurrency через semaphore
let semaphore = DispatchSemaphore(value: 3)
for url in urls {
pool.async {
semaphore.wait()
download(url)
semaphore.signal()
}
}
Конфігурація thread pool безпосередньо впливає на продуктивність застосунку. Невірні параметри призводять до недоутилізації CPU (занадто мало потоків) або перевантаження системи (занадто багато). Для мобільних застосунків оптимальні значення відрізняються від серверних через обмежені ресурси та енергоспоживання. Основні параметри: corePoolSize, maxPoolSize, queue capacity та keepAliveTime.
Формула для IO-bound завдань: corePoolSize = кількість ядер CPU × 2 (потоки очікують введення-виведення). Для CPU-bound завдань: corePoolSize = кількість ядер CPU (потоки постійно завантажені обчисленнями). На сучасних мобільних пристроях (6-8 ядер) це дає 6-8 потоків для CPU-bound і 12-16 для IO-bound. Практичні тести показують, що для типового мобільного застосунку 3-4 потоки оптимальні — більше потоків збільшують енергоспоживання без приросту продуктивності.
Розмір черги завдань (work queue) визначає, скільки завдань може очікувати виконання. Необмежена черга (LinkedBlockingQueue без ліміту) може призвести до OOM при швидкому надходженні завдань. Обмежена черга (ArrayBlockingQueue з фіксованим розміром) відхиляє завдання при переповненні. Для мобільних застосунків рекомендується ArrayBlockingQueue з ємністю 16-32 завдань. CallerRunsPolicy — найкращий RejectedExecutionHandler для мобільних: він сповільнює відправника (pressure back) замість втрати завдання.
| Параметр | Рекомендація для мобільних | Обґрунтування |
|---|---|---|
| corePoolSize | 2-4 | Обмежені ресурси мобільного пристрою |
| maxPoolSize | corePoolSize (або +1-2) | Уникнути пікових навантажень на створення потоків |
| keepAliveTime | 15-30 секунд | Швидке звільнення пам'яті, але без частого створення |
| Queue capacity | 16-32 | Баланс між буферизацією та OOM-ризиком |
| Handler | CallerRunsPolicy | Backpressure без втрати завдань |
Розробники мобільних застосунків часто допускають помилки при використанні thread pool, які призводять до падінь, витоків пам'яті та нестабільної роботи. Найчастіші: невиклик shutdown() для ExecutorService, створення нового пулу на кожну операцію, занадто великий пул, deadlock між завданнями, використання CachedThreadPool на Android.
Deadlock виникає, коли завдання в пулі очікує результат іншого завдання з того ж пулу, але всі потоки зайняті очікуванням. Приклад: завдання А відправляє завдання Б в той же пул і викликає future.get() — якщо пул вичерпаний, завдання А чекає на завдання Б, а завдання Б не може виконатися, оскільки немає вільних потоків. Рішення: використовуйте окремі пули для різних рівнів завдань або async callback замість блокуючого .get().
// Deadlock в Thread Pool
val pool = Executors.newFixedThreadPool(1)
// Завдання А чекає на завдання Б — deadlock!
val futureA = pool.submit {
// Це завдання ніколи не виконається
val futureB = pool.submit { 42 }
futureB.get() // Блокується назавжди
}
// Виправлення: окремі пули
val workerPool = Executors.newFixedThreadPool(2)
val callbackPool = Executors.newSingleThreadExecutor()
workerPool.submit {
callbackPool.submit {
// Виконується в окремому пулі — deadlock неможливий
}
}
// Або використовуйте CompletableFuture
workerPool.submit {
CompletableFuture
.supplyAsync { 42 }
.thenAccept { result ->
println(result)
}
}
ExecutorService, створений в Activity, повинен бути завершений в onDestroy(). Якщо цього не зробити, потоки будуть висіти в пам'яті навіть після знищення Activity. Рішення: зберігайте пул в Application scope або ViewModel, а не в Activity. Для корутин використовуйте viewModelScope або lifecycleScope. Якщо пул створено всередині Activity, обов'язково викличте pool.shutdown() в onDestroy(). Для тестування використовуйте es.shutdownNow() для негайної зупинки.
Часті запитання
Thread Pool перевикористовує вже створені потоки для виконання багатьох завдань. Звичайний потік (raw Thread) створюється, виконує одне завдання та знищується. Створення потоку займає близько 1 МБ пам'яті та ~1 мс часу. Thread Pool зменшує накладні витрати, обмежує максимальну кількість потоків та надає API для керування (shutdown, awaitTermination).
Для типового мобільного застосунку оптимально 2-4 потоки. Для CPU-bound завдань — кількість ядер CPU. Для IO-bound завдань — кількість ядер × 2. Більша кількість потоків збільшує енергоспоживання та контекстне перемикання без приросту продуктивності. На Android використовуйте Process.availableProcessors() для визначення ядер. На iOS — ProcessInfo.processInfo.processorCount.
CachedThreadPool створює потоки за потреби та перевикористовує існуючі. Проблема: він не обмежує максимальну кількість потоків. Якщо 100 завдань прийдуть одночасно, буде створено 100 потоків. Це призводить до OOM на Android (кожен потік ~1 МБ). Використовуйте newFixedThreadPool(n) з явним лімітом. CachedThreadPool допустимий лише для короткочасних burst-завдань з гарантією невеликого обсягу.
Так, якщо пул не належить керованому контейнеру (як корутини). shutdown() зупиняє прийом нових завдань і завершує потоки після виконання поточних. Без shutdown() потоки висять у пам'яті, застосунок не завершується. Для Activity викликайте в onDestroy(). Для ViewModel використовуйте coroutineScope. Завершення пулу — обов'язкова частина керування ресурсами, аналогічна закриттю Cursor або InputStream.
Так, OperationQueue та DispatchQueue — це thread pool, що надаються iOS. OperationQueue обмежує concurrency через maxConcurrentOperationCount. DispatchQueue.global() використовує системний пул потоків без прямого контролю. На відміну від Java ThreadPoolExecutor, ви не керуєте corePoolSize або queue capacity — система оптимізує пул автоматично під поточне навантаження та енергоспоживання пристрою.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також