Thread — a processzoridő alapegysége, amelynek saját verme van, és más szálaktól függetlenül fut. A mobilfejlesztésben a szálakat a feladatok párhuzamos végrehajtására használják, hogy a felület hosszú műveletek során is reszponzív maradjon. Az Android támogatja a java.lang.Thread-et, az Executors-t és a Kotlin Coroutines-t, az iOS — a Thread-et (Objective-C), a GCD-t és az OperationQueue-t. A Android Thread Documentation szerint egy natív szál létrehozásához az operációs rendszernek ~1 MB-ot kell elkülönítenie a verem számára.
Lényeg
Thread (végrehajtási szál) — független utasítássorozat, amelyet az operációs rendszer a CPU magjára ütemezhet. Minden folyamat (alkalmazás) legalább egy szálat tartalmaz — a Main Thread-et. A további szálak a feladatok párhuzamos végrehajtására jönnek létre. Minden szálnak saját programverme (lokális változókkal), programszámlálója (PC) és regiszterei vannak. A heap memória közös a folyamat összes szálában.
A mobil operációs rendszerekben a szálakat preemptív multitaskinggal ütemezik: az operációs rendszer bármikor megszakíthatja egy szál végrehajtását, és átadhatja a vezérlést egy másiknak (context switch). A kontextusváltás költséges művelet (1–10 mikroszekundum), mert a CPU regiszterek mentését/visszaállítását, a TLB frissítését és a gyorsítótárak kiürítését igényli. Éppen ezért a túlzott számú szál (több száz és több ezer) rontja a teljesítményt — az operációs rendszer több időt tölt váltással, mint végrehajtással.
Szál és folyamat különböző fogalmak. A folyamat az alkalmazás egy példánya elkülönített virtuális memóriával. A folyamaton belüli szál ezt a memóriát megosztja a többi szállal. Androidon az alkalmazás minden komponense (Activity, Service, BroadcastReceiver) egy folyamatban működik, de különböző szálakon futhat. Az iOS-alkalmazás szintén egyetlen folyamat, amely lehetőséget ad további szálak létrehozására a GCD vagy a Thread révén.
Minden szál a Java/Kotlin (Android) és az NSThread (iOS) esetében öt állapoton megy keresztül: New (létrehozva), Runnable (végrehajtásra kész), Running (a CPU-n fut), Blocked/Waiting (erőforrásra vagy értesítésre vár), Terminated (befejezve). Az állapotok közötti átmeneteket az operációs rendszer ütemezője és a szinkronizálási primitívek irányítják. A fejlesztő befolyásolhatja a szál prioritását (Thread.setPriority()) és állapotát (sleep, join, interrupt).
Androidon a szál Blocked állapotba kerül, amikor foglalt monitort próbál megszerezni (synchronized), Object.wait() vagy Thread.sleep() hívásakor. iOS-en — NSCondition.wait(), pthread_cond_wait() vagy dispatch_semaphore_wait() hívásakor. Blocked állapotban a szál nem fogyaszt CPU-t, de memóriát foglal (verem). A szál másik szálból megszakítható (interrupted), InterruptedException (Java) fogadásával vagy isCancelled (Kotlin Coroutines) ellenőrzésével.
| Állapot | Leírás | Átmenet módja |
|---|---|---|
| New | A szál létrehozva, de nem indítva | Thread() konstruktor |
| Runnable | A szál végrehajtásra kész, CPU-ra vár | thread.start() |
| Running | A szál a CPU magján fut | Az operációs rendszer ütemezője |
| Blocked/Waiting | A szál erőforrásra, monitorra vagy értesítésre vár | synchronized, wait(), sleep() |
| Terminated | A szál befejezte a run()-t, vagy megszakították | run() befejezve, interrupt() |
Context switch (kontextusváltás) — az a művelet, amelynek során az operációs rendszer elmenti az aktuális szál állapotát (regiszterek, PC, TLB), és betölti egy másik elmentett állapotát. A mobil rendszerekben (Linux + ART, iOS-hez XNU) a context switch 1–10 mikroszekundumig tart. Ha egy szál 100 mikroszekundum alatt hajt végre egy feladatot, a context switch pedig 5-ig tart, akkor az idő 5%-a vész el. A context switch minimalizálásához az iOS work stealing-el használja a GCD-t, az Android pedig fixedThreadCount-poolokat.
Az Android az alacsony szintű java.lang.Thread-től a modern korutinokig fejlődött. Az absztrakció minden szintje több lehetőséget ad kisebb többletköltséggel. A Thread az alapsztály, de közvetlen létrehozása nem ajánlott: az új szálat nem kezeli pool, nehéz nyomon követni és megszakítani. Az AsyncTask (API 30-tól deprecated) előrelépés volt, de memóriavesztéstől és a konfigurációk kényelmetlen kezelésétől szenvedett.
A HandlerThread — a Thread speciális alosztálya Looper-rel, amely üzenetsort tud feldolgozni. Háttérszálon futó feladatok szekvenciális végrehajtására használják, például adatok Room-ba vagy fájlokba írására. A HandlerThread a start() hívással jön létre, majd a Handler(handlerThread.looper) révén üzeneteket és Runnable-t lehet küldeni. A handlerThread.quit() hívás leállítja a Looper-t és befejezi a szálat.
// Android: Thread, HandlerThread és Executors
import android.os.Handler
import android.os.HandlerThread
import java.util.concurrent.Executors
class ThreadExample {
// 1. A Thread közvetlen létrehozása (nem ajánlott)
fun directThread() {
val thread = Thread(Runnable {
Thread.sleep(1000)
print("Közvetlen szál végrehajtva")
})
thread.start()
}
// 2. HandlerThread szekvenciális háttérfeladatokhoz
fun handlerThreadExample() {
val handlerThread = HandlerThread("BackgroundQueue")
handlerThread.start()
val handler = Handler(handlerThread.looper)
handler.post {
// Szekvenciális végrehajtás háttérszálon
Thread.sleep(500)
print("HandlerThread: a feladat végrehajtva")
}
// Szál leállítása (akkor fut le, amikor a feladatok befejeződnek)
handlerThread.quitSafely()
}
// 3. Executors — szálpool
fun executorExample() {
val executor = Executors.newFixedThreadPool(4)
for (i in 1..10) {
executor.execute {
print("Task $i on thread ${Thread.currentThread().getName()}")
}
}
executor.shutdown()
}
// 4. Kotlin Coroutines — modern szabvány
suspend fun coroutineExample() = kotlinx.coroutines.withContext(
kotlinx.coroutines.Dispatchers.Default
) {
print("Coroutine on thread: ${Thread.currentThread().getName()}")
}
}
A ThreadExample példa mind a négy szálabsztrakciós szintet megmutatja Androidon. A Thread közvetlen létrehozása a legalacsonyabb szintű és leghatékonyatlanabb megközelítés. A HandlerThread hasznos a háttérben futó szekvenciális feladatokhoz. Az Executors.newFixedThreadPool(4) 4 szálból álló poolt hoz létre legfeljebb 10 feladat párhuzamos végrehajtásához. A Kotlin Coroutines a Dispatchers.Default-tal — modern, hatékony és biztonságos módszer.
HandlerThread — a Thread specializált alosztálya beépített Looper-rel és üzenetsorral. A start() hívással jön létre, majd a Handler(handlerThread.looper) révén Runnable és üzenetek küldhetők. A HandlerThread a feladatokat szigorúan szekvenciálisan hajtja végre — a következő feladat nem kezdődik el az előző befejezéséig. Ez kényelmes a Room-ba vagy fájlokba íráskor, ahol a műveletek sorrendje kritikus. A quitSafely() hívás az aktuális feladat befejezése után állítja le a Looper-t.
Az iOS is három szintet kínál a szálak kezelésére. Thread (Swiftben Thread, Objective-C-ben NSThread) — alacsony szintű API, amely közvetlenül hoz létre natív szálat. GCD (Grand Central Dispatch) a DispatchQueue-n keresztül — az iOS-fejlesztők fő eszköze, amely automatikusan kezeli a szálpoolt. OperationQueue — magas szintű absztrakció a GCD felett, függőségek, prioritások és megszakítás támogatásával.
A Thread közvetlen használata a modern iOS-fejlesztésben rendkívül ritka — a GCD minden szükséges lehetőséget biztosít automatikus memória- és szálkezeléssel. A Thread csak speciális esetekben használatos: thread-local storage (threadDictionary) beállítása, RunLoop létrehozása háttérszálhoz, vagy pthread_t-t váró C könyvtárakkal való integráció.
import Foundation
class ThreadManager {
// 1. Thread (alacsony szintű)
func createThread() {
let thread = Thread {
// A kód új szálon fut
print("Current thread: \(Thread.current)")
}
thread.name = "com.app.worker"
thread.qualityOfService = .utility
thread.start()
}
// 2. GCD — DispatchQueue
func gcdExample() {
// Párhuzamos sor
let queue = DispatchQueue(label: "com.app.concurrent",
qos: .utility,
attributes: .concurrent)
queue.async {
print("GCD async feladat")
}
// Barrier az írás szinkronizálásához
queue.async(flags: .barrier) {
// Exkluzív hozzáférés írás közben
print("Barrier write: exkluzív hozzáférés")
}
}
// 3. OperationQueue függőségekkel
func operationQueueExample() {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2
queue.qualityOfService = .background
let download = BlockOperation {
print("Letöltés...")
}
let process = BlockOperation {
print("Feldolgozás...")
}
let save = BlockOperation {
print("Mentés...")
}
// Függőségek: download -> process -> save
process.addDependency(download)
save.addDependency(process)
queue.addOperations([download, process, save], waitUntilFinished: false)
}
}
// Thread-safe gyűjtemény GCD barrieren keresztül
class ThreadSafeArray<T> {
private var array: [T] = []
private let queue = DispatchQueue(label: "com.app.concurrent",
attributes: .concurrent)
var count: Int {
return queue.sync { array.count } // concurrent read
}
func append(_ element: T) {
queue.async(flags: .barrier) { // exclusive write
self.array.append(element)
}
}
}
A ThreadSafeArray osztály a Concurrent Read / Exclusive Write mintát mutatja be GCD barrieren keresztül. Az olvasás a queue.sync{} révén párhuzamosan, több szálból történik. Az írás a queue.async(flags: .barrier) révén minden más műveletet (olvasást és írást egyaránt) blokkol az írás befejezéséig. Ez hatékonyabb, mint a synchronized blokkok, mert nem blokkolja az olvasókat, amíg nincs írás.
A Thread közvetlen használata iOS-en három esetben indokolt: thread-local storage (Thread.current.threadDictionary) — a szálhoz kötött adatok tárolása; speciális RunLoop létrehozása háttérszálon performSelector:onThread: paranccsal; a pthread_t-t váró C/C++ könyvtárakkal való integráció. Minden más esetben a GCD a DispatchQueue-n keresztül előnyösebb — automatikusan kezeli a szálpoolt és az energiafogyasztást.
Race condition (versenyhelyzet) akkor alakul ki, ha két vagy több szál egyszerre fér hozzá megosztott adatokhoz, és legalább az egyik írást végez. Az eredmény a végrehajtás sorrendjétől (timing) függ, és kiszámíthatatlan. A race condition megelőzéséhez szinkronizálási primitíveket használnak. A mobilfejlesztésben elérhetők zárak (synchronized, NSLock), atomi műveletek (AtomicInteger, iOS atomic tulajdonságok) és sorok (serial queue).
A primitív kiválasztása a forgatókönyvtől függ. Egyszerű számlálókhoz és zászlókhoz az atomi műveletek (AtomicInteger, atomic property) elegendők. Több műveletet tartalmazó kritikus szakaszokhoz — zárak (synchronized, NSLock). Összetett adatszerkezetekhez — serial DispatchQueue vagy GCD barrier. A zárakat könnyebb megérteni, de hajlamosak a deadlockra és livelockra. A sorok bonyolultabbak, de biztonságosabbak.
// Szinkronizálás Android/Kotlin környezetben
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
class Counter {
// 1. AtomicInteger — egyszerű számlálókhoz
private val atomicCount = AtomicInteger(0)
fun incrementAtomic() = atomicCount.incrementAndGet()
// 2. synchronized — kritikus szakaszokhoz
@Synchronized
fun synchronizedOperation() {
// Egyszerre csak egy szál
doWork()
}
// 3. Mutex korutinokból — suspend-safe
private val mutex = Mutex()
suspend fun mutexOperation() {
mutex.withLock {
// protected code — thread-safe
doWork()
}
}
private fun doWork() { /* critical section */ }
}
// Deadlock-példa: A blokkolja B-t, B blokkolja A-t
class DeadlockExample {
private val lockA = Any()
private val lockB = Any()
fun methodA() = synchronized(lockA) {
Thread.sleep(100)
synchronized(lockB) { print("OK") }
}
fun methodB() = synchronized(lockB) {
Thread.sleep(100)
synchronized(lockA) { print("OK") }
}
}
A Counter három szinkronizálási megközelítést mutat be. AtomicInteger.incrementAndGet() — zár nélküli atomi művelet (CAS). @Synchronized — a Java beépített monitorja, a teljes objektumot blokkolja. Mutex.withLock — korutinos mutex, a szál blokkolása helyett a korutint függeszti fel (hatékonyabb). A DeadlockExample klasszikus deadlockot mutat: két szál különböző sorrendben szerzi meg a zárakat.
Thread Pool (szálpool) — előre létrehozott szálak halmaza, amelyeket a feladatok végrehajtására újrahasznosítanak. Ahelyett, hogy minden feladathoz új szálat hoznának létre (költséges), a pool szabad szálat vesz a poolból. Ha nincs szabad szál, a feladat sorba kerül. A pool automatikusan kezeli a méretet: csúcs terhelésnél új szálak jönnek létre, az üresjáratban lévő szálak befejeződnek. Ez tízszeresére csökkenti a szállétrehozás többletköltségét.
Androidon az Executors.newFixedThreadPool(4) 4 szálból álló poolt hoz létre. Ha egyszerre 10 feladat érkezik, 4 azonnal elindul, 6 pedig a sorban várakozik. Az Executors.newCachedThreadPool() szükség szerint (korlát nélkül) hoz létre szálakat, és 60 másodperc után befejezi az üresjáratban lévőket. Az iOS-hez a GCD automatikusan biztosítja a globális sorok pooljait, amelyek mérete a CPU-magok számához és az aktuális terheléshez igazodik.
A Kotlin Coroutines esetében a szálpoolok a diszpécserek belsejében vannak elrejtve. A Dispatchers.Default a CPU-magok számával megegyező méretű poolt használ (minimum 2). A Dispatchers.IO — 64 szál (elég több száz IO-bound feladathoz, mert a többség CPU-foglalás nélkül vár a be- és kimenetre). Minden diszpécser automatikusan méretezi a poolt a terheléshez, üresjáratban energiát takarítva meg.
Gyakran ismételt kérdések
Thread — a kód végrehajtásának alapegysége egy alkalmazásban. Minden folyamatnak sok szálja lehet, amelyek megosztják a memóriát, de saját vermük van. A mobilfejlesztésben a szálakat a feladatok párhuzamos végrehajtására használják a UI blokkolása nélkül. Az Android Thread-et, Executors-t, HandlerThread-et és Coroutines-t használ. Az iOS Thread-et, GCD-t (DispatchQueue) és OperationQueue-t használ.
Thread létrehozása ~1 MB verem elkülönítését igényli Androidon és ~512 KB-ot iOS-en — ez költséges művelet. 1000 feladat esetén 1000 szál közvetlen létrehozása ~1 GB-ot igényel csak a vermekre, plusz a context switch többletköltségét. A Thread helyett használjon poolokat (Executors, GCD) vagy korutinokat — ezek újrahasznosítják a szálakat, tízszeresére csökkentve a többletköltséget.
Race condition — kiszámíthatatlan viselkedés, amikor több szál egyszerre fér hozzá megosztott adatokhoz írással. Három módon kerülhető el: atomi típusok (AtomicInteger), zárak (synchronized, NSLock) használatával, vagy a hozzáférés soron keresztüli szerializálásával (serial DispatchQueue, Actor Kotlinban). A legjobb gyakorlat — a megosztott mutable állapot minimalizálása és az immutability használata.
Thread — natív rendszerobjektum, amely ~1 MB vermet foglal, és az operációs rendszer magjához kötött. Korutin — a Kotlin könnyű végrehajtási egysége, amely nem kötődik konkrét szálhoz, és blokkolás nélkül felfüggeszthető (suspend). Egyetlen szál több ezer korutint hajthat végre. A korutinok memóriahatékonyabbak, és lehetővé teszik aszinkron kód írását callbacks nélkül.
Deadlock az alkalmazás teljes lefagyásaként nyilvánul meg ANR nélkül. Androidon a Thread.getAllStackTraces() segítségével dumpolja az összes szál vermét — két szál egymás zárjaira fog várni. iOS-en — Thread.callStackSymbols. Eszközök: Android Studio Profiler (Threads fül), Instruments (iOS, Thread State View). Megelőzés: rögzített sorrendben vegye meg a zárakat, használjon tryLock-ot időtúllépéssel.
Összegzé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