Thread — de basiseenheid van processortijd, met een eigen stack, die onafhankelijk van andere threads draait. In mobiele ontwikkeling worden threads gebruikt voor parallelle uitvoering van taken, zodat de interface responsief blijft tijdens langdurige bewerkingen. Android ondersteunt java.lang.Thread, Executors en Kotlin Coroutines, iOS — Thread (Objective-C), GCD en OperationQueue. Volgens Android Thread Documentation vereist het maken van een native thread dat het besturingssysteem ~1 MB voor de stack toewijst.
Belangrijkste punten
Thread (uitvoeringsdraad) — een onafhankelijke reeks instructies die het besturingssysteem op een CPU-kern kan plannen. Elk proces (app) bevat ten minste één thread — Main Thread. Extra threads worden aangemaakt voor parallelle uitvoering van taken. Elke thread heeft een eigen programmastack (met lokale variabelen), een programmateller (PC) en registers. Het heap-geheugen wordt gedeeld door alle threads van het proces.
In mobiele besturingssystemen worden threads gepland via preëmptieve multitasking: het besturingssysteem kan de uitvoering van een thread op elk moment onderbreken en de controle overdragen aan een andere (context switch). Het wisselen van context is een dure bewerking (1–10 microseconden), omdat het CPU-registers moet opslaan/herstellen, de TLB moet bijwerken en caches moet legen. Precies daarom verslechtert een overmatig aantal threads (honderden en duizenden) de prestaties — het besturingssysteem besteedt meer tijd aan wisselen dan aan uitvoeren.
Thread en proces zijn verschillende begrippen. Een proces is een instantie van een app met toegewezen virtueel geheugen. Een thread binnen een proces deelt dat geheugen met andere threads. In Android werkt elk component van de app (Activity, Service, BroadcastReceiver) in één proces, maar kan in verschillende threads worden uitgevoerd. Een iOS-app is ook één proces met de mogelijkheid om extra threads te maken via GCD of Thread.
Elke thread in Java/Kotlin (Android) en NSThread (iOS) doorloopt vijf toestanden: New (aangemaakt), Runnable (klaar voor uitvoering), Running (draait op de CPU), Blocked/Waiting (wacht op een bron of melding), Terminated (beëindigd). De overgangen tussen toestanden worden beheerd door de scheduler van het besturingssysteem en synchronisatie-primitieven. De ontwikkelaar kan de prioriteit van de thread (Thread.setPriority()) en de toestand ervan (sleep, join, interrupt) beïnvloeden.
In Android gaat een thread naar de toestand Blocked bij een poging om een bezette monitor te veroveren (synchronized), bij een aanroep van Object.wait() of Thread.sleep(). In iOS — bij een aanroep van NSCondition.wait(), pthread_cond_wait() of dispatch_semaphore_wait(). In de toestand Blocked verbruikt de thread geen CPU, maar wel geheugen (stack). Een thread kan vanuit een andere thread worden onderbroken (interrupted), met een InterruptedException (Java) of door isCancelled te controleren (Kotlin Coroutines).
| Toestand | Beschrijving | Overgangsmethode |
|---|---|---|
| New | Thread is aangemaakt, maar niet gestart | Thread() constructor |
| Runnable | Thread is klaar voor uitvoering, wacht op CPU | thread.start() |
| Running | Thread draait op een CPU-kern | Scheduler van het besturingssysteem |
| Blocked/Waiting | Thread wacht op een bron, monitor of melding | synchronized, wait(), sleep() |
| Terminated | Thread heeft run() voltooid of is onderbroken | run() voltooid, interrupt() |
Context switch (contextwisseling) — de bewerking waarbij het besturingssysteem de toestand van de huidige thread (registers, PC, TLB) opslaat en de opgeslagen toestand van een andere laadt. In mobiele systemen (Linux + ART, XNU voor iOS) duurt een context switch 1–10 microseconden. Als een thread een taak in 100 microseconden uitvoert en een context switch 5 duurt, gaat 5% van de tijd verloren. Om context switches te minimaliseren gebruikt iOS GCD met work stealing, en Android pools met fixedThreadCount.
Android is geëvolueerd van het laagniveau java.lang.Thread naar moderne coroutines. Elk abstractieniveau biedt meer mogelijkheden met lagere overhead. Thread is de basisklasse, maar directe aanmaak wordt niet aangeraden: de nieuwe thread wordt niet door een pool beheerd en is moeilijk te monitoren en te annuleren. AsyncTask (deprecated vanaf API 30) was een stap vooruit, maar leed aan geheugenlekken en onhandige verwerking van configuraties.
HandlerThread — een speciale subklasse van Thread met een Looper, die een berichtenwachtrij kan verwerken. Wordt gebruikt voor sequentiële uitvoering van taken op een achtergrondthread, bijvoorbeeld het wegschrijven van gegevens naar Room of bestanden. HandlerThread wordt aangemaakt met een aanroep van start(), waarna via Handler(handlerThread.looper) berichten en Runnable kunnen worden verzonden. De aanroep handlerThread.quit() stopt de Looper en beëindigt de thread.
// Android: Thread, HandlerThread en Executors
import android.os.Handler
import android.os.HandlerThread
import java.util.concurrent.Executors
class ThreadExample {
// 1. Direct aanmaken van Thread (niet aanbevolen)
fun directThread() {
val thread = Thread(Runnable {
Thread.sleep(1000)
print("Directe thread uitgevoerd")
})
thread.start()
}
// 2. HandlerThread voor sequentiële achtergrondtaken
fun handlerThreadExample() {
val handlerThread = HandlerThread("BackgroundQueue")
handlerThread.start()
val handler = Handler(handlerThread.looper)
handler.post {
// Sequentiële uitvoering op de achtergrondthread
Thread.sleep(500)
print("HandlerThread: taak uitgevoerd")
}
// Thread stoppen (uitgevoerd wanneer de taken zijn voltooid)
handlerThread.quitSafely()
}
// 3. Executors — threadpool
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 — moderne standaard
suspend fun coroutineExample() = kotlinx.coroutines.withContext(
kotlinx.coroutines.Dispatchers.Default
) {
print("Coroutine on thread: ${Thread.currentThread().getName()}")
}
}
Het voorbeeld ThreadExample toont alle vier abstractieniveaus van threads in Android. Directe aanmaak van Thread is de laagstniveau- en inefficiëntste benadering. HandlerThread is nuttig voor sequentiële taken op de achtergrond. Executors.newFixedThreadPool(4) maakt een pool van 4 threads voor parallelle uitvoering van maximaal 10 taken. Kotlin Coroutines met Dispatchers.Default — een moderne, efficiënte en veilige methode.
HandlerThread — een gespecialiseerde subklasse van Thread met ingebouwde Looper en berichtenwachtrij. Wordt aangemaakt met een aanroep van start(), waarna via Handler(handlerThread.looper) Runnable en berichten kunnen worden verzonden. HandlerThread voert taken strikt sequentieel uit — de volgende taak begint niet voordat de vorige is voltooid. Dit is handig bij het wegschrijven van gegevens naar Room of bestanden, waar de volgorde van bewerkingen cruciaal is. De aanroep quitSafely() stopt de Looper na voltooiing van de huidige taak.
iOS biedt ook drie niveaus van werken met threads. Thread (Thread in Swift, NSThread in Objective-C) — een laagniveau-API die rechtstreeks een native thread maakt. GCD (Grand Central Dispatch) via DispatchQueue — het belangrijkste hulpmiddel voor iOS-ontwikkelaars, dat automatisch de threadpool beheert. OperationQueue — een hoogniveau-abstractie boven GCD met ondersteuning voor afhankelijkheden, prioriteiten en annulering.
Direct gebruik van Thread in moderne iOS-ontwikkeling komt uiterst zelden voor — GCD biedt alle benodigde mogelijkheden met automatisch geheugen- en threadbeheer. Thread wordt alleen gebruikt voor specifieke gevallen: instellen van thread-local storage (threadDictionary), aanmaken van een RunLoop voor een achtergrondthread of integratie met C-bibliotheken die pthread_t verwachten.
import Foundation
class ThreadManager {
// 1. Thread (laag niveau)
func createThread() {
let thread = Thread {
// De code wordt op een nieuwe thread uitgevoerd
print("Current thread: \(Thread.current)")
}
thread.name = "com.app.worker"
thread.qualityOfService = .utility
thread.start()
}
// 2. GCD — DispatchQueue
func gcdExample() {
// Parallelle wachtrij
let queue = DispatchQueue(label: "com.app.concurrent",
qos: .utility,
attributes: .concurrent)
queue.async {
print("GCD async-taak")
}
// Barrier voor synchronisatie van schrijven
queue.async(flags: .barrier) {
// Exclusieve toegang tijdens het schrijven
print("Barrier write: exclusieve toegang")
}
}
// 3. OperationQueue met afhankelijkheden
func operationQueueExample() {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2
queue.qualityOfService = .background
let download = BlockOperation {
print("Bezig met downloaden...")
}
let process = BlockOperation {
print("Bezig met verwerken...")
}
let save = BlockOperation {
print("Bezig met opslaan...")
}
// Afhankelijkheden: download -> process -> save
process.addDependency(download)
save.addDependency(process)
queue.addOperations([download, process, save], waitUntilFinished: false)
}
}
// Thread-safe collectie via GCD-barrier
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)
}
}
}
De klasse ThreadSafeArray demonstreert het patroon Concurrent Read / Exclusive Write via een GCD-barrier. Lezen via queue.sync{} gebeurt parallel vanuit meerdere threads. Schrijven via queue.async(flags: .barrier) blokkeert alle andere bewerkingen (zowel lezen als schrijven) totdat het schrijven is voltooid. Dit is efficiënter dan synchronized-blokken, omdat lezers niet worden geblokkeerd zolang er geen schrijfactie is.
Direct gebruik van Thread in iOS is gerechtvaardigd in drie gevallen: voor thread-local storage (Thread.current.threadDictionary) — het opslaan van gegevens die aan de thread zijn gebonden; voor het aanmaken van een speciale RunLoop op een achtergrondthread met performSelector:onThread:; voor integratie met C/C++-bibliotheken die pthread_t verwachten. In alle andere gevallen heeft GCD via DispatchQueue de voorkeur — het beheert automatisch de threadpool en het energieverbruik.
Race condition (raceconditie) ontstaat wanneer twee of meer threads tegelijkertijd toegang hebben tot gedeelde gegevens en ten minste één van hen schrijft. Het resultaat hangt af van de uitvoeringsvolgorde (timing) en is onvoorspelbaar. Om een race condition te voorkomen worden synchronisatie-primitieven gebruikt. In mobiele ontwikkeling zijn vergrendelingen (synchronized, NSLock), atomaire bewerkingen (AtomicInteger, atomic-eigenschappen in iOS) en wachtrijen (serial queue) beschikbaar.
De keuze van het primitief hangt af van het scenario. Voor eenvoudige tellers en vlaggen zijn atomaire bewerkingen (AtomicInteger, atomic property) voldoende. Voor kritieke secties met meerdere bewerkingen — vergrendelingen (synchronized, NSLock). Voor complexe datastructuren — serial DispatchQueue of GCD-barrier. Vergrendelingen zijn gemakkelijker te begrijpen, maar zijn gevoelig voor deadlock en livelock. Wachtrijen zijn complexer, maar veiliger.
// Synchronisatie in Android/Kotlin
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
class Counter {
// 1. AtomicInteger — voor eenvoudige tellers
private val atomicCount = AtomicInteger(0)
fun incrementAtomic() = atomicCount.incrementAndGet()
// 2. synchronized — voor kritieke secties
@Synchronized
fun synchronizedOperation() {
// Slechts één thread tegelijk
doWork()
}
// 3. Mutex uit coroutines — suspend-safe
private val mutex = Mutex()
suspend fun mutexOperation() {
mutex.withLock {
// protected code — thread-safe
doWork()
}
}
private fun doWork() { /* critical section */ }
}
// Deadlock-voorbeeld: A vergrendelt B, B vergrendelt A
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") }
}
}
Counter demonstreert drie benaderingen van synchronisatie. AtomicInteger.incrementAndGet() — een atomaire bewerking zonder vergrendelingen (CAS). @Synchronized — de ingebouwde monitor van Java, vergrendelt het hele object. Mutex.withLock — een coroutine-mutex, schort de coroutine op in plaats van de thread te blokkeren (efficiënter). DeadlockExample toont een klassieke deadlock: twee threads veroveren vergrendelingen in verschillende volgorde.
Thread Pool (threadpool) — een set van vooraf aangemaakte threads die worden hergebruikt voor het uitvoeren van taken. In plaats van voor elke taak een nieuwe thread te maken (duur), neemt de pool een vrije thread uit de pool. Als er geen vrije threads zijn, wordt de taak in een wachtrij geplaatst. De pool beheert automatisch de grootte: nieuwe threads worden aangemaakt bij piekbelasting, inactieve threads worden beëindigd. Dit vermindert de overhead van het aanmaken van threads tientallen keren.
In Android maakt Executors.newFixedThreadPool(4) een pool van 4 threads. Als er tegelijkertijd 10 taken binnenkomen, starten er 4 direct en wachten er 6 in de wachtrij. Executors.newCachedThreadPool() maakt threads naar behoefte (zonder limiet) en beëindigt inactieve threads na 60 seconden. Voor iOS biedt GCD automatisch pools van globale wachtrijen, waarvan de omvang overeenkomt met het aantal CPU-kernen en de huidige belasting.
In Kotlin Coroutines zijn threadpools verborgen in dispatchers. Dispatchers.Default gebruikt een pool ter grootte van het aantal CPU-kernen (minimaal 2). Dispatchers.IO — 64 threads (genoeg voor honderden IO-bound taken, omdat de meeste op invoer-uitvoer wachten zonder CPU te bezetten). Elke dispatcher schaalt de pool automatisch naar de belasting, waardoor batterij-energie wordt bespaard tijdens inactiviteit.
Veelgestelde vragen
Thread — de basiseenheid van uitvoering van code in een app. Elk proces kan veel threads hebben die het geheugen delen, maar een eigen stack hebben. In mobiele ontwikkeling worden threads gebruikt voor parallelle uitvoering van taken zonder de UI te blokkeren. Android gebruikt Thread, Executors, HandlerThread en Coroutines. iOS gebruikt Thread, GCD (DispatchQueue) en OperationQueue.
Thread aanmaken vereist het toewijzen van ~1 MB voor de stack in Android en ~512 KB in iOS — dit is een dure bewerking. Voor 1000 taken vereist het direct aanmaken van 1000 threads ~1 GB alleen voor de stacks, plus overhead voor context switches. Gebruik in plaats van Thread pools (Executors, GCD) of coroutines — zij hergebruiken threads en verminderen de overhead tientallen keren.
Race condition — onvoorspelbaar gedrag bij gelijktijdige toegang van meerdere threads tot gedeelde gegevens met schrijven. Het kan op drie manieren worden voorkomen: gebruik van atomaire typen (AtomicInteger), vergrendelingen (synchronized, NSLock) of serialisatie van toegang via een wachtrij (serial DispatchQueue, Actor in Kotlin). Beste praktijk — minimaliseer gedeelde mutable toestand en gebruik immutability.
Thread — een native systeemobject dat ~1 MB stack in beslag neemt en is gebonden aan de kernel van het besturingssysteem. Coroutine — een lichtgewicht uitvoeringseenheid in Kotlin die niet aan een specifieke thread is gebonden en kan worden opgeschort (suspend) zonder te blokkeren. Eén thread kan duizenden coroutines uitvoeren. Coroutines zijn efficiënter qua geheugen en maken het schrijven van asynchrone code zonder callbacks mogelijk.
Deadlock manifesteert zich als het volledig vastlopen van de app zonder ANR. Gebruik in Android Thread.getAllStackTraces() om de stacks van alle threads te dumpen — twee threads wachten dan op elkaars vergrendelingen. In iOS — Thread.callStackSymbols. Hulpmiddelen: Android Studio Profiler (tabblad Threads), Instruments (iOS, Thread State View). Preventie: verover vergrendelingen in een vaste volgorde, gebruik tryLock met een timeout.
Conclusies
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook