Thread Pool — то је механизам управљања нитима, при чему се унапред креирани пул нити поново користи за извршавање задатака, избегавајући додатне трошкове креирања и уништавања нити. У мобилном развоју, пул нити се користи за позадинске операције: мрежне захтеве, обраду слика, рад са базама података. Према Google Android Documentation (2025), ExecutorService је препоручени начин управљања позадинским нитима у Android-у. У iOS-у аналогну улогу имају OperationQueue и GCD DispatchQueue са глобалним конкурентним редовима.
Главно
Thread Pool (пул нити) — архитектонски образац у којем се фиксни број нити креира унапред и поново користи за извршавање више задатака. Уместо креирања нове нити за сваку операцију (што је скупо: око 1 MB стека по нити у JVM-у), задаци се смештају у ред и извршавају их слободне нити из пула. У мобилном развоју, пул нити је критичан за перформансе — 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) |
| Потрошња меморије | Расте са сваким задатком | Фиксна |
Пул нити ради по принципу 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. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође