Thread Pool — ez egy szálkezelő mechanizmus, amelyben egy előre létrehozott szálkészlet újra felhasználható feladatok végrehajtására, elkerülve a szálak létrehozásának és megsemmisítésének többletköltségeit. A mobilfejlesztésben a szálkészlet háttérműveletekhez használatos: hálózati kérések, képfeldolgozás, adatbázisokkal való munka. A Google Android Documentation (2025) szerint az ExecutorService az ajánlott módja a háttérszálak kezelésének Androidban. Az iOS-ben hasonló szerepet töltenek be az OperationQueue és a GCD DispatchQueue globális egyidejű sorokkal.
Főbb pontok
Thread Pool (szálkészlet) — egy architekturális minta, amelyben egy rögzített számú szálat előre létrehoznak és újra felhasználnak több feladat végrehajtásához. Ahelyett, hogy minden művelethez új szálat hoznának létre (ami költséges: körülbelül 1 MB verem szálanként a JVM-ben), a feladatokat egy sorba helyezik, és a készletből szabad szálak hajtják végre. A mobilfejlesztésben a szálkészlet kritikus fontosságú a teljesítmény szempontjából — az Android és az iOS korlátozza az alkalmazásonkénti szálak számát.
A szál létrehozása költséges művelet: veremfoglalás, rendszerbe regisztrálás, kontextusváltás. A korlátozott erőforrásokkal rendelkező mobileszközökön a szálak ellenőrizetlen létrehozása OOM-hoz (OutOfMemoryError) vezet Androidban és throttlinghoz iOS-ben. Thread Pool mindkét problémát megoldja: korlátozza az egyidejűleg futó szálak maximális számát és újra felhasználja a már létrehozott szálakat. A Google az ExecutorService-t ajánlja a raw Thread() helyett, az Apple az OperationQueue-t ajánlja a Thread helyett.
| Paraméter | Készlet nélkül (raw Thread) | Thread Pool-lal |
|---|---|---|
| Szál létrehozása | Minden feladathoz | Egyszer a készlet létrehozásakor |
| Maximum szálak | Korlátlan (OOM kockázat) | Core/max pool size által korlátozva |
| Kihasználtság | Alacsony (a szál meghal a feladat után) | Magas (a szál újra felhasználható) |
| Kezelés | Kézi (join, interrupt) | Automatikus (ExecutorService) |
| Memóriafogyasztás | Minden feladattal nő | Rögzített |
A szálkészlet a Producer-Consumer elv alapján működik: a feladatokat (Runnable/Callable) egy blokkoló sorba (BlockingQueue) helyezik. A készletből származó szálak a sorban várják a feladatokat, és elviszik azokat végrehajtásra. Algoritmus: ha kevesebb a szabad szál, mint a corePoolSize, új szál jön létre. Ha a corePoolSize elérte, a feladat a sorba kerül. Ha a sor megtelt és kevesebb a szál, mint a maximumPoolSize, egy további szál jön létre. A maximumPoolSize túllépésekor a feladat elutasításra kerül a RejectedExecutionHandler segítségével.
Core pool size — a készletben még tétlen állapotban is fenntartott szálak száma. Maximum pool size — a sor túlcsordulásakor létrehozható szálak maximális száma. A köztük lévő különbség a további (overflow) szálak, amelyek ideiglenesen jönnek létre és az idle időtúllépés után befejeződnek. Mobileszközökön ajánlott a corePoolSize-t a maximumPoolSize-szal egyenlőre állítani a szálak létrehozásának csúcsterhelésének elkerülése érdekében.
BlockingQueue tárolja a végrehajtásra váró feladatokat. A legnépszerűbb implementációk: LinkedBlockingQueue (korlátlan), ArrayBlockingQueue (korlátozott) és SynchronousQueue (tárolás nélkül — a feladat azonnal átadásra kerül a szálnak). A sor és a készlet túlcsordulásakor a RejectedExecutionHandler aktiválódik. Szabványos házirendek: AbortPolicy (RejectedExecutionException-t dob), CallerRunsPolicy (a feladó szálában hajtja végre), DiscardPolicy és DiscardOldestPolicy.
// Thread Pool létrehozása Androidban
val threadPool = ThreadPoolExecutor(
corePoolSize = 2, // Minimum 2 szál
maximumPoolSize = 4, // Maximum 4 szál
keepAliveTime = 30L, // Túlcsordulási szál élettartama
unit = TimeUnit.SECONDS,
workQueue = LinkedBlockingQueue<Runnable>(16),
threadFactory = Executors.defaultThreadFactory(),
handler = ThreadPoolExecutor.CallerRunsPolicy()
)
// Feladatok elküldése
threadPool.execute {
val result = api.fetchData()
runOnUiThread { showData(result) }
}
// Készlet lezárása
threadPool.shutdown()
// Az összes feladat befejezésének várása
threadPool.awaitTermination(10, TimeUnit.SECONDS)
Az Android több szálkészlet-implementációt kínál a java.util.concurrent segítségével. Executors — egy gyár kész konfigurációkkal: newFixedThreadPool(n) (rögzített készlet), newCachedThreadPool() (korlátlan, szálak szükség szerint jönnek létre), newSingleThreadExecutor() (egy szál — szekvenciális végrehajtás). Mobil projektekhez a newFixedThreadPool ésszerű korláttal (2-4 szál) ajánlott, mivel a cached pool túl sok szálat hozhat létre.
ThreadPoolExecutor (TPE) — az ExecutorService teljes implementációja konfigurálható paraméterekkel. Androidban a TPE az AsyncTask, IntentService és JobIntentService belső működésében használatos. A corePoolSize, maximumPoolSize, keepAliveTime, BlockingQueue és RejectedExecutionHandler paraméterek lehetővé teszik a készlet viselkedésének finomhangolását. Ajánlások Androidhoz: corePoolSize = CPU magok száma - 1 (IO-bound feladatokhoz) vagy magok száma (CPU-bound feladatokhoz). Tipikus alkalmazásokhoz — 2-4 szál.
// Kész Executors konfigurációk
// 1. Fix készlet 3 szálra
val fixedPool = Executors.newFixedThreadPool(3)
// 2. Gyorsítótárazott készlet (nem ajánlott mobilra)
val cachedPool = Executors.newCachedThreadPool()
// 3. Egyetlen szál (szerializáció)
val singlePool = Executors.newSingleThreadExecutor()
// 4. Ütemező (periodikus feladatok)
val scheduler = Executors.newScheduledThreadPool(2)
// Használat Callable és Future segítségével
val future: Future<String> = fixedPool.submit(Callable {
"Result: ${api.call()}"
})
// Eredmény lekérése (blokkolja a szálat)
val result = future.get(5, TimeUnit.SECONDS)
// Készlet lezárása
fixedPool.shutdownNow()
Kotlin korutinok CoroutineDispatcher-t biztosítanak — a szálkészlethez hasonló absztrakciót. A Dispatchers.IO egy 64 szálból álló készletet (korlátozott) használ. A Dispatchers.Default — a CPU magok számával megegyező készlet. A CoroutineDispatcher nem igényel kézi shutdown-t és automatikusan kezelődik. Finomhangoláshoz hozza létre saját ExecutorCoroutineDispatcher-jét az Executors.newFixedThreadPool(2).asCoroutineDispatcher() segítségével. A korutinok nem helyettesítik a szálkészletet, hanem beburkolják azt.
Az iOS két fő mechanizmust kínál a szálkészlet kezelésére: OperationQueue (GCD-alapú magas szintű API) és GCD DispatchQueue (alacsony szintű C-API). Az OperationQueue a szálkészletet a maxConcurrentOperationCount tulajdonság segítségével inkapszulálja. Alapértelmezés szerint az OperationQueue a system-defined maximum-ot használja (a rendszer terhelésétől függően). A DispatchQueue.global() egy egyidejű sort biztosít a rendszer szálkészletével.
OperationQueue a szálkészletet a maxConcurrentOperationCount segítségével kezeli. Az 1-es érték soros sort hoz létre (hasonlóan a single thread pool-hoz). Az 1-nél nagyobb érték — egyidejű készlet a megadott korláttal. Alapértelmezés szerint maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount (rendszer optimum, általában 4-8 szál). Az Operation támogatja a függőségeket, prioritásokat és megszakítást. Minden művelet a rendszerkészlet bármely szabad szálán végrehajtódik.
// OperationQueue 3 szálas készlettel
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 3
queue.qualityOfService = .utility
// Műveletek létrehozása
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) }
}
// Függőség: operation2 vár operation1-re
operation2.addDependency(operation1)
// Hozzáadás a sorhoz
queue.addOperations([operation1, operation2], waitUntilFinished: false)
// Összes művelet megszakítása
queue.cancelAllOperations()
DispatchQueue — az Apple szálkészlete. Az egyidejű sor (qos: .utility) a rendszer szálkészletét használja, amely az eszköz aktuális terheléséhez van optimalizálva. A különböző QoS-ek (userInteractive, userInitiated, utility, background) különböző prioritású készletekre vannak leképezve. DispatchGroup lehetővé teszi több feladat szinkronizálását. A DispatchWorkItem támogatja a megszakítást és a qualityOfService-t. Finom vezérléshez hozza létre saját egyidejű sorait a DispatchQueue(label: qos: attributes: .concurrent) segítségével.
// GCD DispatchQueue szálkészletként
let customQueue = DispatchQueue(
label: "com.app.background",
qos: .utility,
attributes: .concurrent,
autoreleaseFrequency: .workItem
)
// Feladatok küldése a készletbe
customQueue.async { self.processFile(file1) }
customQueue.async { self.processFile(file2) }
// DispatchGroup a szinkronizációhoz
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() // Mindkét feladat befejeződött
}
// Egyidejűség korlátozása semaphore segítségével
let semaphore = DispatchSemaphore(value: 3)
for url in urls {
pool.async {
semaphore.wait()
download(url)
semaphore.signal()
}
}
A szálkészlet konfigurációja közvetlenül befolyásolja az alkalmazás teljesítményét. Helytelen paraméterek a CPU alulhasználtságához (túl kevés szál) vagy a rendszer túlterheléséhez (túl sok szál) vezetnek. Mobil alkalmazásokban az optimális értékek eltérnek a szerver értékektől a korlátozott erőforrások és energiafogyasztás miatt. Fő paraméterek: corePoolSize, maxPoolSize, queue capacity és keepAliveTime.
Képlet IO-bound feladatokhoz: corePoolSize = CPU magok száma × 2 (a szálak bemenet-kimenetre várnak). CPU-bound feladatokhoz: corePoolSize = CPU magok száma (a szálak folyamatosan számításokkal vannak elfoglalva). Modern mobileszközökön (6-8 mag) ez 6-8 szálat ad CPU-bound és 12-16-ot IO-bound esetén. Gyakorlati tesztek azt mutatják, hogy egy tipikus mobil alkalmazáshoz 3-4 szál optimális — több szál növeli az energiafogyasztást a teljesítmény növelése nélkül.
A feladatsor (work queue) mérete határozza meg, hogy hány feladat várhat végrehajtásra. Korlátlan sor (LinkedBlockingQueue korlát nélkül) OOM-hoz vezethet a feladatok gyors érkezésekor. Korlátozott sor (ArrayBlockingQueue rögzített mérettel) elutasítja a feladatokat túlcsorduláskor. Mobil alkalmazásokhoz 16-32 feladat kapacitású ArrayBlockingQueue ajánlott. CallerRunsPolicy — a legjobb RejectedExecutionHandler mobileszközökhöz: lelassítja a feladót (pressure back) a feladat elvesztése helyett.
| Paraméter | Ajánlás mobilhoz | Indoklás |
|---|---|---|
| corePoolSize | 2-4 | Mobileszköz korlátozott erőforrásai |
| maxPoolSize | corePoolSize (vagy +1-2) | Szálak létrehozásának csúcsterhelésének elkerülése |
| keepAliveTime | 15-30 másodperc | Gyors memóriafelszabadítás, de gyakori létrehozás nélkül |
| Queue capacity | 16-32 | Egyensúly a pufferelés és az OOM kockázat között |
| Handler | CallerRunsPolicy | Visszanyomás feladatvesztés nélkül |
A mobil alkalmazásfejlesztők gyakran követnek el hibákat a szálkészlet használata során, amelyek összeomlásokhoz, memóriaszivárgásokhoz és instabil működéshez vezetnek. Leggyakoribbak: a shutdown() nem hívása ExecutorService esetén, új készlet létrehozása minden művelethez, túl nagy készlet, deadlock a feladatok között, CachedThreadPool használata Androidban.
Deadlock akkor fordul elő, amikor egy készletben lévő feladat egy másik feladat eredményére vár ugyanabból a készletből, de minden szál a várakozással van elfoglalva. Példa: az A feladat elküldi a B feladatot ugyanabba a készletbe és meghívja a future.get()-et — ha a készlet kimerült, az A feladat vár a B feladatra, a B feladat pedig nem tud végrehajtódni, mert nincs szabad szál. Megoldás: használjon külön készleteket a feladatok különböző szintjeihez vagy aszinkron callback-et a blokkoló .get() helyett.
// Deadlock a Thread Poolban
val pool = Executors.newFixedThreadPool(1)
// A feladat vár B feladatra — deadlock!
val futureA = pool.submit {
// Ez a feladat soha nem fog végrehajtódni
val futureB = pool.submit { 42 }
futureB.get() // Örökké blokkolva
}
// Javítás: külön készletek
val workerPool = Executors.newFixedThreadPool(2)
val callbackPool = Executors.newSingleThreadExecutor()
workerPool.submit {
callbackPool.submit {
// Külön készletben hajtódik végre — deadlock lehetetlen
}
}
// Vagy használjon CompletableFuture-t
workerPool.submit {
CompletableFuture
.supplyAsync { 42 }
.thenAccept { result ->
println(result)
}
}
Az Activity-ben létrehozott ExecutorService-t be kell fejezni az onDestroy()-ben. Ha ez nem történik meg, a szálak a memóriában maradnak az Activity megsemmisítése után is. Megoldás: tárolja a készletet Application scope-ban vagy ViewModel-ben, ne az Activity-ben. Korutinokhoz használjon viewModelScope-t vagy lifecycleScope-ot. Ha a készlet az Activity-n belül lett létrehozva, feltétlenül hívja meg a pool.shutdown()-ot az onDestroy()-ben. Teszteléshez használja az es.shutdownNow()-t az azonnali leállításhoz.
Gyakran ismételt kérdések
Thread Pool újra felhasználja a már létrehozott szálakat több feladat végrehajtásához. Egy hétköznapi szál (raw Thread) létrejön, végrehajt egy feladatot és megsemmisül. Egy szál létrehozása körülbelül 1 MB memóriát és ~1 ms időt vesz igénybe. A Thread Pool csökkenti a többletköltségeket, korlátozza a szálak maximális számát és kezelési API-t (shutdown, awaitTermination) biztosít.
Egy tipikus mobil alkalmazáshoz 2-4 szál optimális. CPU-bound feladatokhoz — a CPU magok száma. IO-bound feladatokhoz — a magok száma × 2. Több szál növeli az energiafogyasztást és a kontextusváltást a teljesítmény növelése nélkül. Androidban a Process.availableProcessors() segítségével határozza meg a magokat. iOS-ben — a ProcessInfo.processInfo.processorCount segítségével.
CachedThreadPool szükség szerint hoz létre szálakat és újra felhasználja a meglévőket. Probléma: nem korlátozza a szálak maximális számát. Ha 100 feladat érkezik egyszerre, 100 szál jön létre. Ez OOM-hoz vezet Androidban (minden szál ~1 MB). Használjon newFixedThreadPool(n)-t explicit korláttal. A CachedThreadPool csak rövid idejű burst feladatokhoz engedélyezett kis mennyiség garanciájával.
Igen, ha a készlet nem tartozik egy felügyelt tárolóhoz (mint a korutinok). A shutdown() leállítja az új feladatok fogadását és befejezi a szálakat az aktuális feladatok befejezése után. A shutdown() nélkül a szálak a memóriában maradnak, az alkalmazás nem fejeződik be. Activity esetén hívja az onDestroy()-ben. ViewModel esetén használjon coroutineScope-ot. A készlet befejezése az erőforrás-kezelés kötelező része, hasonlóan a Cursor vagy InputStream bezárásához.
Igen, az OperationQueue és a DispatchQueue az iOS által biztosított szálkészlet. Az OperationQueue a maxConcurrentOperationCount segítségével korlátozza az egyidejűséget. A DispatchQueue.global() a rendszer szálkészletét használja közvetlen vezérlés nélkül. A Java ThreadPoolExecutorral ellentétben nem kezeli a corePoolSize-t vagy a queue capacity-t — a rendszer automatikusan optimalizálja a készletet az aktuális terhelés és az eszköz energiafogyasztása alapján.
Összefoglalás
Kulcsrakész mobilalkalmazást fejlesztünk
Az IT Sectr 2017 óta készít iOS és Android alkalmazásokat induló vállalkozásoknak és vállalkozásoknak. Tanácsot adunk, és a legjobb megoldást javasoljuk.
Olvassa el is