Thread Pool у мобилном развоју — основе, пул нити и принцип рада

Аутор: IT Sectr Објављено: 2026-03-18 Време читања: 11 мин

Thread Pool — то је механизам управљања нитима, при чему се унапред креирани пул нити поново користи за извршавање задатака, избегавајући додатне трошкове креирања и уништавања нити. У мобилном развоју, пул нити се користи за позадинске операције: мрежне захтеве, обраду слика, рад са базама података. Према Google Android Documentation (2025), ExecutorService је препоручени начин управљања позадинским нитима у Android-у. У iOS-у аналогну улогу имају OperationQueue и GCD DispatchQueue са глобалним конкурентним редовима.

Главно

  • Thread Pool — пул нити које се могу поново користити за извршавање позадинских задатака без додатних трошкова креирања нити.
  • ExecutorService у Android-у управља пулом преко ThreadPoolExecutor-а са подесивим параметрима.
  • OperationQueue у iOS-у инкапсулира пул нити кроз maxConcurrentOperationCount.
  • Core pool size — минималан број нити увек спремних за извршавање задатака.
  • Work queue чува задатке који чекају на слободну нит у пулу.

Шта је Thread Pool?

Thread Pool (пул нити) — архитектонски образац у којем се фиксни број нити креира унапред и поново користи за извршавање више задатака. Уместо креирања нове нити за сваку операцију (што је скупо: око 1 MB стека по нити у JVM-у), задаци се смештају у ред и извршавају их слободне нити из пула. У мобилном развоју, пул нити је критичан за перформансе — Android и iOS ограничавају број нити по апликацији.

Зашто је Thread Pool важан у мобилном развоју

Креирање нити је скупа операција: алокација стека, регистрација у систему, контекстно пребацивање. На мобилним уређајима са ограниченим ресурсима, неконтролисано креирање нити доводи до 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 vs Maximum Pool Size

Core pool size — број нити које се одржавају у пулу чак и у стању мировања. Maximum pool size — максималан број нити које се могу креирати при препуњавању реда. Разлика између њих су додатне (overflow) нити које се креирају привремено и завршавају након idle тајмаута. На мобилним уређајима препоручује се постављање corePoolSize једнаког maximumPoolSize-у како би се избегла оптерећења при креирању нити.

Work Queue и RejectedExecutionHandler

BlockingQueue чува задатке који чекају на извршење. Најпопуларније имплементације: LinkedBlockingQueue (неограничен), ArrayBlockingQueue (ограничен) и SynchronousQueue (без складиштења — задатак се одмах прослеђује нити). При препуњавању реда и пула, активира се RejectedExecutionHandler. Стандардне политике: AbortPolicy (баца RejectedExecutionException), CallerRunsPolicy (извршава у нити пошиљаоца), DiscardPolicy и DiscardOldestPolicy.

kotlin
// Креирање 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)

Thread Pool у Android-у: ExecutorService

Android пружа неколико имплементација пула нити преко java.util.concurrent. Executors — фабрика са готовим конфигурацијама: newFixedThreadPool(n) (фиксни пул), newCachedThreadPool() (неограничен, нити се креирају по потреби), newSingleThreadExecutor() (једна нит — секвенцијално извршавање). За мобилне пројекте препоручује се newFixedThreadPool са разумним лимитом (2-4 нити), јер cached пул може створити превише нити.

ThreadPoolExecutor у Android-у

ThreadPoolExecutor (TPE) — потпуна имплементација ExecutorService-а са подесивим параметрима. У Android-у, TPE се користи унутар AsyncTask-а, IntentService-а и JobIntentService-а. Параметри corePoolSize, maximumPoolSize, keepAliveTime, BlockingQueue и RejectedExecutionHandler омогућавају фино подешавање понашања пула. Препоруке за Android: corePoolSize = број CPU језгара - 1 (за IO-bound задатке) или број језгара (за CPU-bound задатке). За типичне апликације — 2-4 нити.

kotlin
// Готове конфигурације 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()

CoroutineDispatcher као пул нити

Kotlin корутине пружају CoroutineDispatcher — апстракцију сличну пулу нити. Dispatchers.IO користи пул од 64 нити (ограничен). Dispatchers.Default — пул једнак броју CPU језгара. CoroutineDispatcher не захтева ручно shutdown и управља се аутоматски. За фино подешавање, креирајте сопствени ExecutorCoroutineDispatcher кроз Executors.newFixedThreadPool(2).asCoroutineDispatcher(). Корутине не замењују пул нити, већ га обавијају.

Thread Pool у iOS-у: OperationQueue и GCD

iOS пружа два главна механизма за управљање пулом нити: OperationQueue (високонивоски API заснован на GCD-у) и GCD DispatchQueue (нисконивоски C-API). OperationQueue инкапсулира пул нити кроз својство maxConcurrentOperationCount. Подразумевано, OperationQueue користи system-defined maximum (зависно од оптерећења система). DispatchQueue.global() пружа конкурентан ред са системским пулом нити.

OperationQueue и maxConcurrentOperationCount

OperationQueue управља пулом нити кроз maxConcurrentOperationCount. Вредност 1 креира серијски ред (аналогно single thread pool-у). Вредност већа од 1 — конкурентан пул са наведеним лимитом. Подразумевано, maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount (системски оптимум, обично 4-8 нити). Operation подржава зависности, приоритете и отказивање. Свака операција се извршава на било којој слободној нити из системског пула.

swift
// 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()

GCD DispatchQueue као пул нити

DispatchQueue — Apple-ов пул нити. Конкурентан ред (qos: .utility) користи системски пул нити, оптимизован за тренутно оптерећење уређаја. Различити QoS (userInteractive, userInitiated, utility, background) мапирају се на различите пулове са различитим приоритетима. DispatchGroup омогућава синхронизацију више задатака. DispatchWorkItem подржава отказивање и qualityOfService. За фину контролу, креирајте сопствене конкурентне редове кроз DispatchQueue(label: qos: attributes: .concurrent).

swift
// 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()
    }
}

Параметри конфигурације 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 нити оптималне — више нити повећава потрошњу енергије без побољшања перформанси.

Queue Capacity и понашање при препуњавању

Величина реда задатака (work queue) одређује колико задатака може чекати на извршење. Неограничен ред (LinkedBlockingQueue без лимита) може довести до OOM-а при брзом пристизању задатака. Ограничен ред (ArrayBlockingQueue са фиксном величином) одбија задатке при препуњавању. За мобилне апликације препоручује се ArrayBlockingQueue капацитета 16-32 задатка. CallerRunsPolicy — најбољи RejectedExecutionHandler за мобилне уређаје: успорава пошиљаоца (pressure back) уместо губљења задатка.

ПараметарПрепорука за мобилнеОбразложење
corePoolSize2-4Ограничени ресурси мобилног уређаја
maxPoolSizecorePoolSize (или +1-2)Избегавање вршних оптерећења при креирању нити
keepAliveTime15-30 секундиБрзо ослобађање меморије, али без честог креирања
Queue capacity16-32Равнотежа између баферисања и OOM ризика
HandlerCallerRunsPolicyПовратни притисак без губитка задатака

Типичне грешке при раду са пулом нити

Програмери мобилних апликација често праве грешке при коришћењу пула нити, које доводе до падова, цурења меморије и нестабилног рада. Најчешће: непозивање shutdown() за ExecutorService, креирање новог пула за сваку операцију, превелик пул, deadlock између задатака, коришћење CachedThreadPool-а на Android-у.

Deadlock у Thread Pool-у

Deadlock настаје када задатак у пулу чека резултат другог задатка из истог пула, али су све нити заузете чекањем. Пример: задатак А шаље задатак Б у исти пул и позива future.get() — ако је пул исцрпљен, задатак А чека задатак Б, а задатак Б не може да се изврши јер нема слободних нити. Решење: користите одвојене пулове за различите нивое задатака или асинхрони callback уместо блокирајућег .get().

kotlin
// 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 разликује од обичне нити?

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 и зашто је опасан на Android-у?

CachedThreadPool креира нити по потреби и поново користи постојеће. Проблем: не ограничава максималан број нити. Ако 100 задатака стигне истовремено, биће креирано 100 нити. То доводи до OOM-а на Android-у (свака нит ~1 MB). Користите newFixedThreadPool(n) са експлицитним лимитом. CachedThreadPool је дозвољен само за краткотрајне burst задатке са гаранцијом мале количине.

Да ли треба позвати shutdown() за ExecutorService?

Да, ако пул не припада управљаном контејнеру (као корутине). shutdown() зауставља пријем нових задатака и завршава нити након извршења текућих. Без shutdown()-а, нити остају у меморији, апликација се не завршава. За Activity позовите у onDestroy(). За ViewModel користите coroutineScope. Завршавање пула је обавезан део управљања ресурсима, аналогно затварању Cursor-а или InputStream-а.

Да ли су OperationQueue и DispatchQueue пул нити?

Да, OperationQueue и DispatchQueue су пул нити које пружа iOS. OperationQueue ограничава конкурентност кроз maxConcurrentOperationCount. DispatchQueue.global() користи системски пул нити без директне контроле. За разлику од Java ThreadPoolExecutor-а, не управљате corePoolSize-ом или queue capacity-ем — систем оптимизује пул аутоматски према тренутном оптерећењу и потрошњи енергије уређаја.

Резиме

  • Thread Pool — пул нити које се могу поново користити за извршавање позадинских задатака, смањујући трошкове креирања нити.
  • Android користи ThreadPoolExecutor и Executors.newFixedThreadPool(n) са експлицитним ограничењем величине пула.
  • iOS пружа OperationQueue са maxConcurrentOperationCount и GCD DispatchQueue са QoS пуловима.
  • Core pool size — минималан број нити; maximum pool size — максималан при препуњавању реда.
  • Deadlock у пулу настаје када задатак чека други задатак из истог пула.
  • CallerRunsPolicy је пожељан за мобилне апликације — успорава пошиљаоца без губитка задатака.
  • Оптимална величина пула за типичну мобилну апликацију је 2-4 нити са затварањем кроз shutdown().

Развићемо мобилну апликацију под кључ

IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.

Разговарајте о пројекту

Прочитајте такође