Background Thread — olyan végrehajtási szál, amely nem kapcsolódik a felhasználói felülethez, hosszú ideig tartó műveletekre szánva: hálózati kérések, fájlokkal való munka, JSON feldolgozás, kép tömörítés, titkosítás és adatbázis lekérdezések. iOS-ben a háttérszálak kezelése GCD (DispatchQueue.global) és OperationQueue segítségével történik, Android-ban — Executors, WorkManager és Kotlin Coroutines (Dispatchers.IO, Dispatchers.Default) révén. A Apple DispatchQueue Documentation szerint a háttérművelet befejezése után az eredményt vissza kell juttatni a Main Thread-ra a felület frissítéséhez.
Fő pontok
Background Thread — bármely szál az alkalmazásban, amely nem a Main Thread és nem fér hozzá a UI-hoz. Feladata, hogy tehermentesítse a fő szálat a nehéz műveletektől, hogy a felület reszponzív maradjon. Az operációs rendszer elosztja a háttérszálakat a processzormagok között, lehetővé téve több feladat párhuzamos végrehajtását. Az iOS automatikusan kezeli a szálpoolt a GCD-n keresztül, az Android — a Java Executors poolokon keresztül.
Ellentétben a Main Thread-tel, amely az eseményeket szekvenciálisan dolgozza fel (egyiket a másik után), a háttérszálak párhuzamosan hajthatók végre, csak a CPU magok száma korlátozza. Például egy 8 magos eszközön akár 8 párhuzamos háttérfeladat is indítható jelentős lassulás nélkül. A túlzott számú szál (több száz) azonban thread starvation-hoz vezet — versengés a magokért és a kontextusváltás (context switch) többletköltségeinek növekedése.
Quality of Service (QoS) — iOS mechanizmus, amely lehetővé teszi a háttérfeladat prioritásának megadását. Értékek: .userInteractive (legmagasabb, majdnem Main Thread), .userInitiated (a felhasználó várja az eredményt), .default (szabványos), .utility (a felhasználó nem vár közvetlenül), .background (legalacsonyabb, szinkronizáláshoz és indexeléshez). Android-ban az analóg a Thread.setPriority() 1-től 10-ig, de az Android cgroups-ot is használ a szálprioritások csoportos kezeléséhez.
DispatchQueue.global(qos:) — az elsődleges módja a háttérsor megszerzésének iOS-ben. A GCD (Grand Central Dispatch) automatikusan létrehoz egy szálpoolt és elosztja a feladatokat a magok között. A DispatchQueue.global(qos: .background).async {} hívás egy végrehajtási blokkot küld a legalacsonyabb prioritású háttérsorba. Azon feladatokhoz, amelyek eredménye azonnal szükséges, használja a .userInitiated vagy .utility értéket.
OperationQueue — egy magasabb szintű absztrakció a GCD felett, amely lehetővé teszi a műveletek közötti függőségek, az egyidejűleg végrehajtható műveletek maximális számának (maxConcurrentOperationCount) és a prioritások megadását. Az OperationQueue kényelmes összetett többfeladatos láncokhoz: fájl letöltése -> kicsomagolás -> gyorsítótárba mentés. Alapértelmezés szerint az OperationQueue háttérszálakat használ, ha másként nincs megadva.
import UIKit
class ImageDownloader {
func downloadImagesSequentially() {
let urls = ["https://example.com/1.png", "https://example.com/2.png"]
// OperationQueue maxConcurrentOperationCount = 2-vel
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2
queue.qualityOfService = .utility
for urlString in urls {
queue.addOperation {
guard let url = URL(string: urlString),
let data = try? Data(contentsOf: url)
else { return }
DispatchQueue.main.async {
print("Betöltve: \(url.lastPathComponent)")
}
}
}
}
// GCD: globális háttérsor különböző QoS-sel
func backgroundTaskWithQoS() {
DispatchQueue.global(qos: .userInitiated).async {
// Magas prioritás — a felhasználó várja az eredményt
let result = self.heavyComputation()
DispatchQueue.main.async {
self.showResult(result)
}
}
}
private func heavyComputation() -> String {
Thread.sleep(forTimeInterval: 2) // munka szimulációja
return "Számítások eredménye"
}
private func showResult(_ result: String) {
print("Result on Main: \(result)")
}
}
A példában az OperationQueue két képet tölt be párhuzamosan (maxConcurrentOperationCount = 2) háttérrel a qualityOfService = .utility segítségével. A GCD backgroundTaskWithQoS metódus a globális sort használja .userInitiated értékkel egy olyan feladathoz, amelynek eredményét a felhasználó várja. Mindkét megközelítés a DispatchQueue.main-ra való visszatéréssel zárul a UI frissítéséhez — ez az iOS kötelező követelménye.
A GCD két típusú sort támogat: serial (szekvenciális) és concurrent (párhuzamos). A serial sorok a feladatokat egyenként hajtják végre — ez kényelmes egy megosztott erőforráshoz (fájl, adatbázis) való hozzáféréshez zárolások nélkül. A concurrent sorok a feladatokat párhuzamosan hajtják végre, elosztva azokat a szabad magok között. A DispatchQueue.global — mindig concurrent. Serial sor létrehozásához használja a DispatchQueue(label: "com.app.queue") parancsot.
Android több absztrakciós szintet kínál a háttérszálakhoz. A klasszikus megközelítés — java.util.concurrent.Executors.newFixedThreadPool(n) vagy Executors.newCachedThreadPool(). A modern megközelítés — Kotlin Coroutines a Dispatchers.IO-val (bemenet-kimenethez: hálózat, fájlok, adatbázis) és Dispatchers.Default-tal (CPU-intenzív feladatokhoz: rendezés, képfeldolgozás). WorkManager — késleltetett és garantált háttérfeladatokhoz.
HandlerThread — egy speciális Android osztály háttérszál létrehozásához saját Looper-rel (üzenetsorral). Az Executors-tól eltérően a HandlerThread lehetővé teszi üzenetek és Runnable küldését Handleren keresztül. Sorba állítást igénylő műveletekhez használatos (pl. szekvenciális írás az adatbázisba). Használat után a quit() vagy quitSafely() metódust kell meghívni az erőforrások felszabadításához.
// Android: Executors és Coroutines Dispatchers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.concurrent.Executors
class DataRepository {
private val ioExecutor = Executors.newFixedThreadPool(4)
// Klasszikus megközelítés Executors segítségével
fun loadDataLegacy(callback: (String) -> Unit) {
ioExecutor.execute {
val result = readFromFile()
val handler = android.os.Handler(android.os.Looper.getMainLooper())
handler.post { callback(result) }
}
}
// Modern megközelítés Coroutines segítségével
suspend fun loadDataCoroutines(): String {
return withContext(Dispatchers.IO) {
// Fájlművelet — háttérpoolban fut
readFromFile()
}
// Az eredmény automatikusan visszakerül a Dispatchers.Main-re
}
// CPU-intenzív feladat a Dispatchers.Default-on
suspend fun processImage(pixels: IntArray): IntArray {
return withContext(Dispatchers.Default) {
// Rendezés, szűrés — a Default poolban fut
pixels.sortedArray()
}
}
private fun readFromFile(): String {
Thread.sleep(1000) // fájlból olvasás szimulációja
return "file_content"
}
fun cleanup() {
ioExecutor.shutdown()
}
}
A DataRepository példa a háttérszálak evolúcióját mutatja Android-ban. A régi loadDataLegacy metódus Executors.newFixedThreadPool(4)-et használ Handler-rel a Main Thread-ra való visszatéréshez. A modern loadDataCoroutines a withContext(Dispatchers.IO)-t használja — a korutin felfüggesztődik munka közben, nem blokkolja a szálat, és automatikusan folytatódik a Main Thread-on. A Dispatchers.Default CPU-bound műveletekhez ajánlott (rendezés, szűrés, adat transzformáció).
Kotlin Coroutines — nem csupán egy módszer a szálakkal való munkára, hanem egy alapvetően más modell: az aszinkron feladatok nem kötődnek egy adott szálhoz, és felfüggeszthetők (suspend) blokkolás nélkül. Ez azt jelenti, hogy háttérmunka közben a korutin nem foglal el egy szálat, hanem felszabadítja azt más feladatok számára. A suspension mechanizmus lehetővé teszi százezernyi egyidejű feladat végrehajtását egy 4-8 szálas poolon thread starvation nélkül.
Három fő dispatcher: Dispatchers.Main (UI, egy szál), Dispatchers.IO (alapértelmezés szerint 64 szál blokkoló műveletekhez: hálózat, fájlok, adatbázis), Dispatchers.Default (megegyezik a CPU magok számával, intenzív számításokhoz). A withContext segítségével történő kombinálásukkal a fejlesztő callback-ek létrehozása nélkül vált a szálak között. A withContext egy suspend függvény, amely nem adja vissza a vezérlést, amíg a feladat be nem fejeződik.
// Korutinok: háttérfeladatok kompozíciója
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
suspend fun loadUserProfile(userId: String): UserProfile =
coroutineScope {
// Adatok párhuzamos betöltése különböző forrásokból
val user = async(Dispatchers.IO) { fetchUser(userId) }
val posts = async(Dispatchers.IO) { fetchPosts(userId) }
val avatar = async(Dispatchers.Default) {
processAvatar(fetchAvatar(userId))
}
// await() — felfüggesztődik az összes feladat befejezéséig
UserProfile(
user = user.await(),
posts = posts.await(),
avatar = avatar.await()
)
}
data class UserProfile(
val user: String,
val posts: List<String>,
val avatar: ByteArray
)
suspend fun fetchUser(id: String): String { delay(300); return "User:$id" }
suspend fun fetchPosts(id: String): List<String> { delay(500); return listOf("Post1") }
suspend fun fetchAvatar(id: String): ByteArray { delay(200); return ByteArray(1024) }
suspend fun processAvatar(data: ByteArray): ByteArray { delay(100); return data }
A loadUserProfile függvény három párhuzamos háttérfeladatot indít az async segítségével. A fetchUser és fetchPosts — IO-bound (hálózat), a Dispatchers.IO-n futnak. A processAvatar — CPU-bound (képfeldolgozás), a Dispatchers.Default-on fut. Az await() felfüggeszti a korutint az összes feladat befejezéséig. A teljes végrehajtási idő a három feladat közül a maximális idővel egyenlő (500 ms a fetchPosts esetében), nem az összegükkel. Ez a coroutines legfőbb előnye a szekvenciális végrehajtással szemben.
Structured concurrency — az az elv, amely szerint minden korutinnak van egy szülő scope-ja, és a szülő törlése automatikusan törli a gyermek korutinokat. Android-ban a lifecycleScope törli az összes korutint az Activity megsemmisülésekor. viewModelScope — a ViewModel tisztításakor. Ez megakadályozza a háttérfeladatok szivárgását: ha a felhasználó bezárta a képernyőt, a háttérkorutin nem folytatja olyan adatok betöltését, amelyekre már senkinek nincs szüksége.
WorkManager — egy Android Jetpack könyvtár olyan háttérfeladatok végrehajtásához, amelyeket az eszköz újraindítása vagy az alkalmazás bezárása után is végre kell hajtani. Az Executors-tól és a korutinoktól eltérően, amelyek az alkalmazás folyamatában élnek, a WorkManager átadja a feladatot egy rendszerdispatchernek, amely garantálja a végrehajtást megfelelő körülmények között (hálózat elérhetősége, akkumulátor szintje, szabad hely). A WorkManager alkalmas adatszinkronizálásra, naplók feltöltésére, biztonsági mentésre.
A WorkManager-ben egy feladat egy osztály, amely a Worker (vagy CoroutineWorker a korutinokhoz) osztályból származik. A Worker.doWork() a WorkManager által biztosított háttérszálon fut. Az eredmény a Result.success(), Result.retry() vagy Result.failure() segítségével kerül visszaadásra. A feladatok láncokba egyesíthetők: oneTimeWorkRequest.andThen(nextRequest).enqueue(). A WorkManager maga választja ki az optimális végrehajtási időt a korlátozások (Constraints) figyelembevételével.
// WorkManager korutinokkal
import android.content.Context
import androidx.work.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class SyncWorker(
appContext: Context,
workerParams: WorkerParameters
) : CoroutineWorker(appContext, workerParams) {
override suspend fun doWork(): Result {
// A Dispatchers.Default-on fut (alapértelmezett)
return withContext(Dispatchers.IO) {
try {
syncDataToServer()
Result.success()
} catch (e: Exception) {
if (runAttemptCount < 3) Result.retry() else Result.failure()
}
}
}
private suspend fun syncDataToServer() {
// Szinkronizálás szimulációja
delay(1000)
}
}
// WorkManager feladat indítása korlátozásokkal
fun scheduleSync(context: Context) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
val syncWork = OneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, java.util.concurrent.TimeUnit.SECONDS)
.build()
WorkManager.getInstance(context).enqueue(syncWork)
}
A SyncWorker a CoroutineWorker — a Worker korutinokat támogató verziójából származik. A doWork() a Dispatchers.Default-on fut, a hálózati műveletekhez IO-ra váltás a withContext segítségével. A Constraints garantálja, hogy a szinkronizálás csak akkor induljon el, ha van hálózat és az akkumulátor szintje nem alacsonyabb az alacsonynál. A BackoffCriteria EXPONENTIAL értékkel növeli az ismételt próbálkozások közötti intervallumot: 10, 20, 40 másodperc.
Rendszeres feladatokhoz (szinkronizálás 15 percenként, analitika küldése óránként) a WorkManager a PeriodicWorkRequestBuilder-t biztosítja. Minimális intervallum — 15 perc. Az OneTimeWorkRequest-tól eltérően a PeriodicWorkRequest nem garantálja az intervallum pontos betartását — a rendszer összevonhat több időszakos feladatot az akkumulátor kímélése érdekében. Pontos intervallumokhoz használja az AlarmManager-t, de vegye figyelembe az Android 12+ korlátozásait a pontos riasztásokra vonatkozóan.
Első hiba — új Thread létrehozása minden feladathoz. A new Thread().start() egy natív szálat hoz létre ~1 MB veremallokációval. 100 párhuzamos feladat esetén ez 100 MB csak a vermekre, plusz a context switch többletköltségei. Használjon szálpoolokat: Executors.newFixedThreadPool(n) (Android) vagy DispatchQueue.global() (iOS) — ezek újrahasznosítják a szálakat, tízszeresére csökkentve a többletköltséget.
Második hiba — mutable állapot elérése több háttérszálból szinkronizálás nélkül. Ha két háttérszál egyszerre ír ugyanabba az ArrayList-be vagy HashMap-be, versenyhelyzet (race condition) alakul ki: ConcurrentModificationException Android-ban, adatsérülés iOS-ben. Megoldás: használjon szálbiztos kollekciókat (ConcurrentHashMap, CopyOnWriteArrayList) vagy szerializálja a hozzáférést egy soron keresztül (DispatchQueue serial).
Harmadik hiba — háttérfeladatok életciklus-kezelés nélkül. Korutin indítása globális scope-ban az Activity vagy ViewModel életciklusához való kötés nélkül szivárgáshoz vezet: a feladat folytatódik a képernyő megsemmisülése után. Android-ban használja a lifecycleScope-ot (Activity/Fragment) vagy a viewModelScope-ot (ViewModel). iOS-ben — weak self a closure-ökben és a feladatok törlése deinit-kor.
Gyakran ismételt kérdések
Background Thread — az a szál, amelyen a UI-hoz nem kapcsolódó műveletek futnak: hálózati kérések, fájlok olvasása/írása, JSON feldolgozás, számítások. Ez tehermentesíti a Main Thread-ot a nehéz munkától, megőrizve a felület reszponzivitását. iOS-ben a háttérszálakat a GCD (DispatchQueue.global) kezeli, Android-ban — az Executors vagy a Kotlin Coroutines (Dispatchers.IO, Dispatchers.Default).
Dispatchers.IO blokkoló bemenet-kimeneti műveletekre szolgál: fájlok olvasása, hálózati kérések, adatbázissal való munka. 64 szálas poolja van. Dispatchers.Default — CPU-intenzív feladatokhoz: rendezés, szűrés, képfeldolgozás. Poolja megegyezik a CPU magok számával. A Dispatchers.Default használata IO műveletekhez blokkolhatja az összes magot, a Dispatchers.IO használata CPU feladatokhoz pedig túlzott számú szálat hozhat létre.
DispatchQueue.global(qos: .background).async { } egy végrehajtási blokkot küld a globális háttérsorba. A háttérmunka befejezése után vissza kell térni a fő szálra a DispatchQueue.main.async { } segítségével a UI frissítéséhez. Szekvenciális háttérfeladatokhoz használja az OperationQueue-t maxConcurrentOperationCount = 1 értékkel vagy a DispatchQueue(label: "serial")-t.
Az ajánlott háttérszálak száma megegyezik a CPU magok számával plusz 1 az IO-bound feladatokhoz. Egy modern 8 magos eszközön ez 9 szál. Több száz szál létrehozása thread starvation-hoz vezet: az operációs rendszer több időt tölt kontextusváltással (context switch), mint a feladatok végrehajtásával. Az iOS-ben a GCD és az Android-ban az Executors automatikusan optimalizálja a szálpoolt az aktuális eszközhöz.
Kotlin Coroutines-ben a Main Thread-ra való visszatérés automatikusan megtörténik, ha a korutin Main-scope-ban lett elindítva (lifecycleScope.launch, viewModelScope.launch). A withContext(Dispatchers.IO) függvény felfüggeszti a korutint az IO szálon, majd a befejezés után automatikusan folytatja azt a dispatcheren, ahol el lett indítva (általában Main). A DispatchQueue.main.async explicit meghívása nem szükséges.
Ö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