Thread Pool — това е механизъм за управление на нишки, при който предварително създаден пул от нишки се използва повторно за изпълнение на задачи, избягвайки допълнителните разходи за създаване и унищожаване на нишки. В мобилното разработване пулът от нишки се използва за фонови операции: мрежови заявки, обработка на изображения, работа с бази данни. Според Google Android Documentation (2025), ExecutorService е препоръчителният начин за управление на фонови нишки в Android. В iOS аналогична роля играят OperationQueue и GCD DispatchQueue с глобални конкурентни опашки.
Основни моменти
Thread Pool (пул от нишки) — това е архитектурен модел, при който фиксиран брой нишки се създават предварително и се използват повторно за изпълнение на множество задачи. Вместо да се създава нова нишка за всяка операция (което е скъпо: около 1 MB стек на нишка в JVM), задачите се поставят в опашка и се изпълняват от свободни нишки от пула. В мобилното разработване пулът от нишки е критичен за производителността — Android и iOS ограничават броя на нишките на приложение.
Създаването на нишка е скъпа операция: разпределяне на стек, регистрация в системата, превключване на контекст. На мобилни устройства с ограничени ресурси неконтролираното създаване на нишки води до OOM (OutOfMemoryError) в Android и до throttling в iOS. Thread Pool решава и двата проблема: ограничава максималния брой едновременно работещи нишки и използва повторно вече създадените нишки. Google препоръчва ExecutorService вместо raw Thread(), Apple препоръчва OperationQueue вместо Thread.
| Параметър | Без пул (raw Thread) | С Thread Pool |
|---|---|---|
| Създаване на нишка | За всяка задача | Веднъж при създаване на пула |
| Максимум нишки | Неограничен (риск от OOM) | Ограничен от core/max pool size |
| Използване | Ниско (нишката умира след задача) | Високо (нишката се използва повторно) |
| Управление | Ръчно (join, interrupt) | Автоматично (ExecutorService) |
| Консумация на памет | Расте с всяка задача | Фиксирана |
Пулът от нишки работи на принципа 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 предоставя няколко имплементации на пул от нишки чрез 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 — абстракция, подобна на пул от нишки. Dispatchers.IO използва пул от 64 нишки (ограничен). Dispatchers.Default — пул, равен на броя CPU ядра. CoroutineDispatcher не изисква ръчно shutdown и се управлява автоматично. За фино настройване създайте свой собствен ExecutorCoroutineDispatcher чрез Executors.newFixedThreadPool(2).asCoroutineDispatcher(). Корутините не заместват пула от нишки, а го обвиват.
iOS предоставя два основни механизма за управление на пула от нишки: OperationQueue (високо ниво API, базирано на GCD) и GCD DispatchQueue (ниско ниво C-API). OperationQueue капсулира пула от нишки чрез свойството maxConcurrentOperationCount. По подразбиране OperationQueue използва system-defined maximum (в зависимост от натоварването на системата). DispatchQueue.global() предоставя конкурентна опашка със системен пул от нишки.
OperationQueue управлява пула от нишки чрез maxConcurrentOperationCount. Стойност 1 създава серийна опашка (подобно на single thread pool). Стойност по-голяма от 1 — конкурентен пул с посочения лимит. По подразбиране 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 — пулът от нишки на Apple. Конкурентна опашка (qos: .utility) използва системния пул от нишки, оптимизиран за текущото натоварване на устройството. Различните QoS (userInteractive, userInitiated, utility, background) се картират към различни пулове с различни приоритети. DispatchGroup позволява синхронизиране на множество задачи. DispatchWorkItem поддържа отмяна и qualityOfService. За фин контрол създайте свои собствени конкурентни опашки чрез DispatchQueue(label: qos: attributes: .concurrent).
// GCD DispatchQueue като пул от нишки
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() // И двете задачи са завършени
}
// Ограничаване на паралелността чрез semaphore
let semaphore = DispatchSemaphore(value: 3)
for url in urls {
pool.async {
semaphore.wait()
download(url)
semaphore.signal()
}
}
Конфигурацията на пула от нишки пряко влияе върху производителността на приложението. Неправилни параметри водят до недостатъчно използване на 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 | Обратен натиск без загуба на задачи |
Разработчиците на мобилни приложения често допускат грешки при използване на пула от нишки, които водят до сривове, изтичане на памет и нестабилна работа. Най-честите: неизвикване на shutdown() за ExecutorService, създаване на нов пул за всяка операция, твърде голям пул, deadlock между задачи, използване на CachedThreadPool в Android.
Deadlock възниква, когато задача в пула чака резултат от друга задача от същия пул, но всички нишки са заети с чакане. Пример: задача А изпраща задача Б в същия пул и извиква future.get() — ако пулът е изчерпан, задача А чака задача Б, а задача Б не може да се изпълни, защото няма свободни нишки. Решение: използвайте отделни пулове за различни нива на задачи или асинхронен 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 или ViewModel, а не в Activity. За корутини използвайте viewModelScope или lifecycleScope. Ако пулът е създаден вътре в Activity, задължително извикайте pool.shutdown() в onDestroy(). За тестване използвайте es.shutdownNow() за незабавно спиране.
Често задавани въпроси
Thread Pool използва повторно вече създадени нишки за изпълнение на множество задачи. Обикновена нишка (raw Thread) се създава, изпълнява една задача и се унищожава. Създаването на нишка отнема около 1 MB памет и ~1 ms време. 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 MB). Използвайте newFixedThreadPool(n) с изричен лимит. CachedThreadPool е позволен само за краткотрайни burst задачи с гаранция за малък обем.
Да, ако пулът не принадлежи на управляван контейнер (като корутини). shutdown() спира приемането на нови задачи и прекратява нишките след завършване на текущите. Без shutdown() нишките остават в паметта и приложението не приключва. За Activity извиквайте в onDestroy(). За ViewModel използвайте coroutineScope. Завършването на пула е задължителна част от управлението на ресурси, аналогично на затваряне на Cursor или InputStream.
Да, OperationQueue и DispatchQueue са пулът от нишки, предоставен от iOS. OperationQueue ограничава конкурентността чрез maxConcurrentOperationCount. DispatchQueue.global() използва системния пул от нишки без пряк контрол. За разлика от Java ThreadPoolExecutor, вие не управлявате corePoolSize или queue capacity — системата автоматично оптимизира пула според текущото натоварване и консумацията на енергия на устройството.
Обобщение
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също