Thread Pool în dezvoltarea mobilă — baze, pool de fire și principiul de funcționare

Autor: IT Sectr Publicat: 2026-03-18 Timp de citire: 11 min

Thread Pool — este un mecanism de gestionare a firelor, în care un pool de fire creat în prealabil este reutilizat pentru executarea sarcinilor, evitând costurile suplimentare de creare și distrugere a firelor. În dezvoltarea mobilă, pool-ul de fire este utilizat pentru operațiuni în fundal: cereri de rețea, procesarea imaginilor, lucrul cu baze de date. Conform Google Android Documentation (2025), ExecutorService este modalitatea recomandată de gestionare a firelor în fundal în Android. În iOS, un rol similar îl au OperationQueue și GCD DispatchQueue cu cozi concurente globale.

Principalele

  • Thread Pool — un pool de fire reutilizabile pentru executarea sarcinilor în fundal fără costuri suplimentare de creare a firelor.
  • ExecutorService în Android gestionează pool-ul prin ThreadPoolExecutor cu parametri configurabili.
  • OperationQueue în iOS încapsulează pool-ul de fire prin maxConcurrentOperationCount.
  • Core pool size — numărul minim de fire întotdeauna pregătite pentru executarea sarcinilor.
  • Work queue stochează sarcinile care așteaptă un fir liber în pool.

Ce este Thread Pool?

Thread Pool (pool de fire) — este un model arhitectural în care un număr fix de fire este creat în avans și reutilizat pentru executarea mai multor sarcini. În loc să se creeze un fir nou pentru fiecare operațiune (ceea ce este costisitor: aproximativ 1 MB de stivă per fir în JVM), sarcinile sunt plasate într-o coadă și executate de firele libere din pool. În dezvoltarea mobilă, pool-ul de fire este critic pentru performanță — Android și iOS limitează numărul de fire per aplicație.

De ce Thread Pool este important în dezvoltarea mobilă

Crearea unui fir este o operațiune costisitoare: alocarea stivei, înregistrarea în sistem, comutarea contextului. Pe dispozitivele mobile cu resurse limitate, crearea necontrolată a firelor duce la OOM (OutOfMemoryError) în Android și la throttling în iOS. Thread Pool rezolvă ambele probleme: limitează numărul maxim de fire care rulează simultan și reutilizează firele deja create. Google recomandă ExecutorService în loc de raw Thread(), Apple recomandă OperationQueue în loc de Thread.

ParametruFără pool (raw Thread)Cu Thread Pool
Crearea firuluiPentru fiecare sarcinăO dată la crearea pool-ului
Maximum de fireNelimitat (risc OOM)Limitat de core/max pool size
UtilizareScăzută (firul moare după sarcină)Ridicată (firul este reutilizat)
GestionareManuală (join, interrupt)Automată (ExecutorService)
Consum de memorieCrește cu fiecare sarcinăFix

Cum funcționează Thread Pool în dezvoltarea mobilă?

Pool-ul de fire funcționează pe principiul Producer-Consumer: sarcinile (Runnable/Callable) sunt plasate într-o coadă blocantă (BlockingQueue). Firele din pool așteaptă sarcinile în coadă și le preiau pentru execuție. Algoritmul: dacă firele libere sunt mai puține decât corePoolSize, se creează un fir nou. Dacă corePoolSize a fost atins, sarcina este plasată în coadă. Dacă coada este plină și firele sunt mai puține decât maximumPoolSize, se creează un fir suplimentar. La depășirea maximumPoolSize, sarcina este respinsă prin RejectedExecutionHandler.

Core Pool Size vs Maximum Pool Size

Core pool size — numărul de fire menținute în pool chiar și în stare de repaus. Maximum pool size — numărul maxim de fire care pot fi create la supraîncărcarea cozii. Diferența dintre ele o reprezintă firele suplimentare (overflow), care sunt create temporar și se termină după expirarea timpului de inactivitate. Pe dispozitivele mobile, se recomandă setarea corePoolSize egal cu maximumPoolSize pentru a evita sarcinile de vârf la crearea firelor.

Work Queue și RejectedExecutionHandler

BlockingQueue stochează sarcinile care așteaptă execuția. Cele mai populare implementări: LinkedBlockingQueue (nelimitată), ArrayBlockingQueue (limitată) și SynchronousQueue (fără stocare — sarcina este transmisă imediat firului). La supraîncărcarea cozii și a pool-ului, se activează RejectedExecutionHandler. Politicile standard: AbortPolicy (aruncă RejectedExecutionException), CallerRunsPolicy (execută în firul expeditorului), DiscardPolicy și DiscardOldestPolicy.

kotlin
// Crearea Thread Pool în Android
val threadPool = ThreadPoolExecutor(
    corePoolSize = 2,        // Minimum 2 fire
    maximumPoolSize = 4,     // Maximum 4 fire
    keepAliveTime = 30L,     // Durata de viață a firului de overflow
    unit = TimeUnit.SECONDS,
    workQueue = LinkedBlockingQueue<Runnable>(16),
    threadFactory = Executors.defaultThreadFactory(),
    handler = ThreadPoolExecutor.CallerRunsPolicy()
)

// Trimiterea sarcinilor
threadPool.execute {
    val result = api.fetchData()
    runOnUiThread { showData(result) }
}

// Terminarea pool-ului
threadPool.shutdown()
// Așteptarea finalizării tuturor sarcinilor
threadPool.awaitTermination(10, TimeUnit.SECONDS)

Thread Pool în Android: ExecutorService

Android oferă mai multe implementări de pool de fire prin java.util.concurrent. Executors — o fabrică cu configurații gata făcute: newFixedThreadPool(n) (pool fix), newCachedThreadPool() (nelimitat, firele se creează după necesitate), newSingleThreadExecutor() (un singur fir — execuție secvențială). Pentru proiectele mobile, se recomandă newFixedThreadPool cu o limită rezonabilă (2-4 fire), deoarece cached pool poate crea prea multe fire.

ThreadPoolExecutor în Android

ThreadPoolExecutor (TPE) — implementarea completă a ExecutorService cu parametri configurabili. În Android, TPE este utilizat în interiorul AsyncTask, IntentService și JobIntentService. Parametrii corePoolSize, maximumPoolSize, keepAliveTime, BlockingQueue și RejectedExecutionHandler permit ajustarea fină a comportamentului pool-ului. Recomandări pentru Android: corePoolSize = numărul de nuclee CPU - 1 (pentru sarcini IO-bound) sau numărul de nuclee (pentru sarcini CPU-bound). Pentru aplicații tipice — 2-4 fire.

kotlin
// Configurații gata făcute Executors
// 1. Pool fix pe 3 fire
val fixedPool = Executors.newFixedThreadPool(3)

// 2. Pool cache-at (nerecomandat pentru mobil)
val cachedPool = Executors.newCachedThreadPool()

// 3. Un singur fir (serializare)
val singlePool = Executors.newSingleThreadExecutor()

// 4. Planificator (sarcini periodice)
val scheduler = Executors.newScheduledThreadPool(2)

// Utilizarea cu Callable și Future
val future: Future<String> = fixedPool.submit(Callable {
    "Result: ${api.call()}"
})
// Obținerea rezultatului (blochează firul)
val result = future.get(5, TimeUnit.SECONDS)

// Terminarea pool-ului
fixedPool.shutdownNow()

CoroutineDispatcher ca pool de fire

Corutinele Kotlin oferă CoroutineDispatcher — o abstractizare similară pool-ului de fire. Dispatchers.IO utilizează un pool de 64 de fire (limitat). Dispatchers.Default — un pool egal cu numărul de nuclee CPU. CoroutineDispatcher nu necesită shutdown manual și este gestionat automat. Pentru ajustare fină, creați propriul ExecutorCoroutineDispatcher prin Executors.newFixedThreadPool(2).asCoroutineDispatcher(). Corutinele nu înlocuiesc pool-ul de fire, ci îl încapsulează.

Thread Pool în iOS: OperationQueue și GCD

iOS oferă două mecanisme principale de gestionare a pool-ului de fire: OperationQueue (API de nivel înalt bazat pe GCD) și GCD DispatchQueue (API de nivel scăzut C). OperationQueue încapsulează pool-ul de fire prin proprietatea maxConcurrentOperationCount. În mod implicit, OperationQueue utilizează system-defined maximum (dependent de încărcarea sistemului). DispatchQueue.global() oferă o coadă concurentă cu pool-ul de fire de sistem.

OperationQueue și maxConcurrentOperationCount

OperationQueue gestionează pool-ul de fire prin maxConcurrentOperationCount. Valoarea 1 creează o coadă serială (similar cu single thread pool). Valoarea mai mare de 1 — pool concurent cu limita specificată. În mod implicit, maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount (optimul sistemului, de obicei 4-8 fire). Operation suportă dependențe, priorități și anulare. Fiecare operațiune este executată pe orice fir liber din pool-ul de sistem.

swift
// OperationQueue cu pool pe 3 fire
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 3
queue.qualityOfService = .utility

// Crearea operațiunilor
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) }
}

// Dependență: operation2 așteaptă operation1
operation2.addDependency(operation1)

// Adăugarea în coadă
queue.addOperations([operation1, operation2], waitUntilFinished: false)

// Anularea tuturor operațiunilor
queue.cancelAllOperations()

GCD DispatchQueue ca pool de fire

DispatchQueue — este pool-ul de fire de la Apple. Coada concurentă (qos: .utility) utilizează pool-ul de fire de sistem, optimizat pentru încărcarea curentă a dispozitivului. Diferitele QoS (userInteractive, userInitiated, utility, background) se mapază pe diferite pool-uri cu priorități diferite. DispatchGroup permite sincronizarea mai multor sarcini. DispatchWorkItem suportă anularea și qualityOfService. Pentru control fin, creați propriile cozi concurente prin DispatchQueue(label: qos: attributes: .concurrent).

swift
// GCD DispatchQueue ca pool de fire
let customQueue = DispatchQueue(
    label: "com.app.background",
    qos: .utility,
    attributes: .concurrent,
    autoreleaseFrequency: .workItem
)

// Trimiterea sarcinilor în pool
customQueue.async { self.processFile(file1) }
customQueue.async { self.processFile(file2) }

// DispatchGroup pentru sincronizare
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() // Ambele sarcini sunt finalizate
}

// Limitarea concurenței prin semaphore
let semaphore = DispatchSemaphore(value: 3)
for url in urls {
    pool.async {
        semaphore.wait()
        download(url)
        semaphore.signal()
    }
}

Parametrii de configurare a Thread Pool

Configurația pool-ului de fire influențează direct performanța aplicației. Parametrii incorecți duc la subutilizarea CPU (prea puține fire) sau supraîncărcarea sistemului (prea multe). În aplicațiile mobile, valorile optime diferă de cele server din cauza resurselor limitate și a consumului de energie. Parametrii principali: corePoolSize, maxPoolSize, queue capacity și keepAliveTime.

Calculul dimensiunii optime a pool-ului

Formula pentru sarcini IO-bound: corePoolSize = numărul de nuclee CPU × 2 (firele așteaptă intrare-ieșire). Pentru sarcini CPU-bound: corePoolSize = numărul de nuclee CPU (firele sunt ocupate constant cu calcule). Pe dispozitivele mobile moderne (6-8 nuclee), aceasta oferă 6-8 fire pentru CPU-bound și 12-16 pentru IO-bound. Testele practice arată că pentru o aplicație mobilă tipică, 3-4 fire sunt optime — mai multe fire cresc consumul de energie fără a îmbunătăți performanța.

Queue Capacity și comportamentul la supraîncărcare

Dimensiunea cozii de sarcini (work queue) determină câte sarcini pot aștepta execuția. Coada nelimitată (LinkedBlockingQueue fără limită) poate duce la OOM la sosirea rapidă a sarcinilor. Coada limitată (ArrayBlockingQueue cu dimensiune fixă) respinge sarcinile la supraîncărcare. Pentru aplicațiile mobile, se recomandă ArrayBlockingQueue cu o capacitate de 16-32 de sarcini. CallerRunsPolicy — cel mai bun RejectedExecutionHandler pentru dispozitivele mobile: încetinește expeditorul (pressure back) în loc să piardă sarcina.

ParametruRecomandare pentru mobileJustificare
corePoolSize2-4Resursele limitate ale dispozitivului mobil
maxPoolSizecorePoolSize (sau +1-2)Evitarea sarcinilor de vârf la crearea firelor
keepAliveTime15-30 secundeEliberarea rapidă a memoriei, dar fără creare frecventă
Queue capacity16-32Echilibrul între bufferizare și riscul OOM
HandlerCallerRunsPolicyBackpressure fără pierderea sarcinilor

Erori tipice la lucrul cu pool-ul de fire

Dezvoltatorii de aplicații mobile fac adesea greșeli la utilizarea pool-ului de fire, care duc la căderi, scurgeri de memorie și funcționare instabilă. Cele mai frecvente: neapelarea shutdown() pentru ExecutorService, crearea unui nou pool pentru fiecare operațiune, pool prea mare, deadlock între sarcini, utilizarea CachedThreadPool pe Android.

Deadlock în Thread Pool

Deadlock apare atunci când o sarcină din pool așteaptă rezultatul altei sarcini din același pool, dar toate firele sunt ocupate cu așteptarea. Exemplu: sarcina A trimite sarcina B în același pool și apelează future.get() — dacă pool-ul este epuizat, sarcina A așteaptă sarcina B, iar sarcina B nu poate fi executată deoarece nu există fire libere. Soluție: utilizați pool-uri separate pentru diferite niveluri de sarcini sau callback asincron în loc de .get() blocant.

kotlin
// Deadlock în Thread Pool
val pool = Executors.newFixedThreadPool(1)

// Sarcina A așteaptă sarcina B — deadlock!
val futureA = pool.submit {
    // Această sarcină nu se va executa niciodată
    val futureB = pool.submit { 42 }
    futureB.get() // Blocat pentru totdeauna
}

// Remediere: pool-uri separate
val workerPool = Executors.newFixedThreadPool(2)
val callbackPool = Executors.newSingleThreadExecutor()

workerPool.submit {
    callbackPool.submit {
        // Se execută într-un pool separat — deadlock imposibil
    }
}

// Sau utilizați CompletableFuture
workerPool.submit {
    CompletableFuture
        .supplyAsync { 42 }
        .thenAccept { result ->
            println(result)
        }
}

Pool neterminat și scurgeri

ExecutorService creat în Activity trebuie să fie terminat în onDestroy(). Dacă nu se face acest lucru, firele vor rămâne în memorie chiar și după distrugerea Activity. Soluție: păstrați pool-ul în domeniul Application sau ViewModel, nu în Activity. Pentru corutine, utilizați viewModelScope sau lifecycleScope. Dacă pool-ul a fost creat în interiorul Activity, apelați obligatoriu pool.shutdown() în onDestroy(). Pentru testare, utilizați es.shutdownNow() pentru oprirea imediată.

Întrebări frecvente

Cu ce se deosebește Thread Pool de un fir obișnuit?

Thread Pool reutilizează firele deja create pentru executarea mai multor sarcini. Un fir obișnuit (raw Thread) este creat, execută o singură sarcină și este distrus. Crearea unui fir ocupă aproximativ 1 MB de memorie și ~1 ms de timp. Thread Pool reduce costurile suplimentare, limitează numărul maxim de fire și oferă API de gestionare (shutdown, awaitTermination).

Câte fire ar trebui să fie în pool pentru o aplicație mobilă?

Pentru o aplicație mobilă tipică, 2-4 fire sunt optime. Pentru sarcini CPU-bound — numărul de nuclee CPU. Pentru sarcini IO-bound — numărul de nuclee × 2. Un număr mai mare de fire crește consumul de energie și comutarea contextului fără a îmbunătăți performanța. Pe Android, utilizați Process.availableProcessors() pentru a determina nucleele. Pe iOS — ProcessInfo.processInfo.processorCount.

Ce este CachedThreadPool și de ce este periculos pe Android?

CachedThreadPool creează fire după necesitate și reutilizează firele existente. Problemă: nu limitează numărul maxim de fire. Dacă 100 de sarcini sosesc simultan, vor fi create 100 de fire. Aceasta duce la OOM pe Android (fiecare fir ~1 MB). Utilizați newFixedThreadPool(n) cu o limită explicită. CachedThreadPool este permis doar pentru sarcini burst de scurtă durată cu garanția unui volum mic.

Este necesar să se apeleze shutdown() pentru ExecutorService?

Da, dacă pool-ul nu aparține unui container gestionat (cum ar fi corutinele). shutdown() oprește primirea de noi sarcini și termină firele după executarea sarcinilor curente. Fără shutdown(), firele rămân în memorie, iar aplicația nu se termină. Pentru Activity, apelați în onDestroy(). Pentru ViewModel, utilizați coroutineScope. Terminarea pool-ului este o parte obligatorie a gestionării resurselor, similară cu închiderea unui Cursor sau InputStream.

OperationQueue și DispatchQueue sunt pool de fire?

Da, OperationQueue și DispatchQueue sunt pool-ul de fire oferit de iOS. OperationQueue limitează concurența prin maxConcurrentOperationCount. DispatchQueue.global() utilizează pool-ul de fire de sistem fără control direct. Spre deosebire de Java ThreadPoolExecutor, nu gestionați corePoolSize sau queue capacity — sistemul optimizează pool-ul automat în funcție de încărcarea curentă și consumul de energie al dispozitivului.

Rezumat

  • Thread Pool — un pool de fire reutilizabile pentru executarea sarcinilor în fundal, reducând costurile de creare a firelor.
  • Android utilizează ThreadPoolExecutor și Executors.newFixedThreadPool(n) cu limitarea explicită a dimensiunii pool-ului.
  • iOS oferă OperationQueue cu maxConcurrentOperationCount și GCD DispatchQueue cu pool-uri QoS.
  • Core pool size — numărul minim de fire; maximum pool size — maximul la supraîncărcarea cozii.
  • Deadlock în pool apare atunci când o sarcină așteaptă o altă sarcină din același pool.
  • CallerRunsPolicy este preferat pentru aplicațiile mobile — încetinește expeditorul fără a pierde sarcini.
  • Dimensiunea optimă a pool-ului pentru o aplicație mobilă tipică este de 2-4 fire cu închidere prin shutdown().

Vom dezvolta o aplicație mobilă la cheie

IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.

Discutați proiectul

Citiți și