Elke mobiele applicatie voert vele taken tegelijkertijd uit: gegevens laden van het netwerk, gebruikersaanrakingen verwerken, de interface animeren en bestanden opslaan. Als al deze code in één thread draait, bevriest de applicatie bij elke netwerkvertraging. Multithreading en gelijktijdigheid zijn sleutelconcepten waarmee de applicatie responsief en efficiënt blijft. In dit artikel behandelen we alle belangrijke hulpmiddelen: van Main Thread en RunLoop tot Kotlin-coroutines en Combine op iOS. Het materiaal is gebaseerd op de officiële Apple GCD-documentatie.
Belangrijkste Punten
Multithreading is het vermogen van een applicatie om meerdere codefragmenten tegelijkertijd uit te voeren. Elk fragment wordt uitgevoerd in een afzonderlijke thread — een lichtgewicht proces met een eigen call-stack. In mobiele ontwikkeling worden threads in twee categorieën verdeeld: Main Thread (UI-thread) en Background Threads (achtergrondthreads).
Het besturingssysteem beheert zelf de verdeling van threads over processorkernen. Moderne apparaten hebben 6–8 kernen, dus parallelle uitvoering kan het werk versnellen. Het maken van threads is echter een dure operatie, dus direct werken met Thread wordt niet aanbevolen. In plaats daarvan worden abstracties van hoger niveau gebruikt: DispatchQueue, OperationQueue, CoroutineDispatcher.
Gelijktijdigheid (Concurrency) is een breder concept dan multithreading. Gelijktijdigheid betekent dat taken "tegelijkertijd" kunnen worden uitgevoerd, zelfs op een enkele kern, door contextwisseling. Asynchronie (Async/Await) is een programmeermodel waarbij een taak geen thread blokkeert, maar de controle teruggeeft terwijl op een resultaat wordt gewacht. Moderne talen (Kotlin, Swift, Dart) hebben ingebouwde ondersteuning voor Async/Await.
Bij IT Sectr besteden we speciale aandacht aan de juiste multithreading-architectuur aan het begin van een project. Fouten in een vroeg stadium leiden tot moeilijk te vinden bugs: dataraces, deadlocks en applicatie-instabiliteit onder belasting. Elk van onze projecten ondergaat een beoordeling van de gelijktijdigheidsarchitectuur in de planningsfase.
Main Thread (hoofdthread) — de enige thread in een mobiele applicatie die toegang heeft tot de UI. Op Android heet dit UI Thread, op iOS — Main Thread. Alle interfacebewerkingen — tekst wijzigen, animaties, aanrakingen verwerken — worden alleen op de Main Thread uitgevoerd. Als een zware bewerking (bestand laden, JSON-parsen) op de hoofdthread wordt uitgevoerd, stopt de interface met reageren. Op Android leidt dit tot ANR (Application Not Responding), op iOS — tot een "bevroren" scherm.
Background Threads (achtergrondthreads) zijn bedoeld voor alles wat niet met de UI te maken heeft: netwerkverzoeken, databasebewerkingen, beeldverwerking, cryptografie. Na voltooiing wordt het resultaat doorgegeven aan de Main Thread voor weergave. Elk platform biedt zijn eigen tools om tussen threads te schakelen: DispatchQueue.main.async op iOS, runOnUiThread of withContext(Dispatchers.Main) op Android.
RunLoop — de gebeurtenisverwerkingslus op de hoofdthread van iOS. RunLoop wacht op gebeurtenissen (aanrakingen, timers, meldingen) en stuurt ze naar de juiste handlers. Op Android is het equivalent Looper, gekoppeld aan elke Main Thread. Main Looper haalt oneindig berichten uit de wachtrij en geeft ze door aan Handler voor verwerking. Inzicht in RunLoop en Looper helpt geheugenlekken en "hakkelen" van de interface te voorkomen.
Grand Central Dispatch (GCD) — een Apple-bibliotheek voor het beheren van multithreading op het niveau van de C-taal. GCD werkt met DispatchQueue — takenwachtrijen. De ontwikkelaar maakt geen threads handmatig aan; GCD beheert een threadpool en verdeelt taken over beschikbare processorkernen. DispatchQueue zijn er in twee typen: Serial Queue (seriële wachtrij — taken worden één voor één uitgevoerd) en Concurrent Queue (gelijktijdige wachtrij — taken kunnen gelijktijdig worden uitgevoerd).
Main DispatchQueue — een seriële wachtrij die aan de hoofdthread is gekoppeld. Global Queues — gelijktijdige wachtrijen met verschillende prioriteiten (QoS — Quality of Service): userInteractive, userInitiated, utility, background. Het kiezen van de juiste QoS is cruciaal voor prestaties: .userInteractive — voor taken die de UI beïnvloeden (animaties, rendering); .background — voor tijdongevoelige taken (synchronisatie, cache opschonen).
OperationQueue — een abstractie bovenop GCD met extra mogelijkheden: taken annuleren, afhankelijkheden tussen bewerkingen instellen, het maximale aantal gelijktijdige bewerkingen controleren. Bewerkingen zijn objecten van de klasse Operation (of BlockOperation). Voorbeeld: als u een afbeelding moet laden, vervolgens een filter moet toepassen en pas daarna moet weergeven — OperationQueue met afhankelijkheden handelt dit perfect af. In GCD zou u deze stappen handmatig moeten synchroniseren met DispatchGroup of semafor.
Async/Await in Swift 5.5+ — een modern alternatief voor GCD. De trefwoorden async en await maken asynchrone code lineair en leesbaar. Functies worden gemarkeerd als async en aanroepen worden afgewacht met await. Het systeem beheert zelf de contextwisseling: standaard wordt een async-functie uitgevoerd op een achtergrondthread, terwijl UI-updates worden uitgevoerd op MainActor. @MainActor — een attribuut dat uitvoering van code op de hoofdthread garandeert.
Coroutines (coroutines) — lichtgewicht threads voor Kotlin ontwikkeld door JetBrains. In tegenstelling tot gewone threads zijn coroutines niet gebonden aan een specifieke Thread. Duizenden coroutines kunnen op meerdere threads worden uitgevoerd zonder significante overhead. CoroutineScope beheert de levenscyclus van coroutines: viewModelScope is gekoppeld aan ViewModel, lifecycleScope — aan Activity/Fragment. Wanneer het bereik wordt vernietigd, worden alle onderliggende coroutines automatisch geannuleerd.
Dispatchers bepalen op welke threadpool de coroutine wordt uitgevoerd: Dispatchers.Main — UI-thread; Dispatchers.IO — voor netwerkverzoeken en schijfbewerkingen; Dispatchers.Default — voor CPU-intensieve berekeningen. Om van dispatcher te wisselen wordt withContext gebruikt. Coroutines ondersteunen gestructureerde gelijktijdigheid: elke coroutine heeft een parent en wanneer de parent wordt geannuleerd, worden alle onderliggende coroutines geannuleerd. Dit voorkomt geheugenlekken en hangende taken.
Flow — een koude asynchrone datastroom uit de coroutines-bibliotheek. Flow geeft waarden sequentieel uit: (1) de producent genereert gegevens, (2) operatoren transformeren de stroom, (3) de collector verbruikt het resultaat. In tegenstelling tot LiveData ondersteunt Flow complexe operator-ketens (map, filter, flatMapConcat, catch) en is volledig threadveilig. StateFlow en SharedFlow — hete varianten van Flow, ideaal voor UI-status en eenmalige gebeurtenissen (Snackbar, navigatie).
Channel — nog een coroutine-abstractie voor het doorgeven van gegevens tussen coroutines. Channel werkt als een wachtrij: één afzender (send) en een of meer ontvangers (receive). Gebufferde kanalen (Channel(UNLIMITED), Channel(BUFFERED)) maken het mogelijk om het gedrag bij overloop te configureren. Channel wordt vaak samen met Flow gebruikt om callback-gebaseerde API's naar coroutines te bridgen: callbackFlow { … }.
Bij IT Sectr gebruiken we actief coroutines en Flow in alle Android-projecten. Dit maakt het mogelijk om asynchrone code te schrijven die er synchroon uitziet, gemakkelijk te testen is (runTest, TestDispatcher) en geen handmatig threadbeheer vereist. Voorbeeld van een eenvoudige coroutine met gegevens laden:
class UserRepository(
private val api: UserApi,
private val dao: UserDao
) {
suspend fun getUsers(): List<User> = withContext(Dispatchers.IO) {
return@withContext try {
val users = api.fetchUsers()
dao.insertAll(users)
users
} catch (e: Exception) {
dao.getAll()
}
}
}
Reactive programmeren — een paradigma waarin gegevens zich verspreiden als asynchrone stromen (Observable, Publisher). RxJava/RxKotlin — de populairste implementatie voor Android, overgezet van .NET Rx. RxSwift — een vergelijkbare bibliotheek voor iOS. Belangrijkste componenten: Observable (gebeurtenisbron), Observer (abonnee), Scheduler (threadbeheer), Operators (stroomtransformatie).
Combine — een Apple-framework voor reactief programmeren geïntroduceerd in iOS 13. Combine gebruikt de protocollen Publisher (uitgever) en Subscriber (abonnee). In tegenstelling tot RxSwift is Combine ingebouwd in de SDK en nauw geïntegreerd met SwiftUI. Operatoren in Combine: map, filter, combineLatest, zip, debounce, throttle — dekken de meeste scenario's: van gegevensbinding aan UI tot debounce van zoekopdrachten.
Future en Promise — patronen voor het werken met een enkel asynchroon resultaat. Future vertegenwoordigt een waarde die later beschikbaar zal zijn. Promise is een belofte om een waarde te leveren. In Rx is dit Single (één succesvol antwoord of fout), in Combine — Future Publisher. In de praktijk zijn Future/Promise handig voor eenmalige API-verzoeken, terwijl Observable/Publisher — voor continue stromen (geolocatie, tekstinvoer).
Callback en Delegate — klassieke patronen voor asynchrone bewerkingen. Callback — een functie die als argument wordt doorgegeven en wordt aangeroepen bij voltooiing van de bewerking. Delegate — een object dat een protocol implementeert met gebeurtenishandlermethoden. Nadeel: "callback hell" (geneste callbacks) en complexiteit van foutafhandeling. NotificationCenter (iOS) en EventBus (Android) — broadcast-gebeurtenismechanismen, nuttig voor losjes gekoppelde communicatie, maar leiden tot impliciete afhankelijkheden.
Multithreading opent de deur naar hoge prestaties, maar creëert tegelijkertijd het risico op moeilijk te vinden fouten. De meest voorkomende: Race Condition (raceconditie), Deadlock (impasse), Livelock (actieve impasse) en Starvation (uithongering van thread). Inzicht in deze problemen is een essentiële vaardigheid voor elke mobiele ontwikkelaar.
Race Condition treedt op wanneer twee of meer threads tegelijkertijd dezelfde gegevens lezen en schrijven zonder synchronisatie. Het resultaat hangt af van welke thread als eerste wordt uitgevoerd. Klassiek voorbeeld: twee threads verhogen een teller. De bewerking "lees → verhoog → schrijf" is niet atomair, dus bij gelijktijdige uitvoering gaat één verhoging "verloren". De oplossing — gebruik atoomaire bewerkingen (AtomicInteger, AtomicReference) of vergrendelingen (Mutex, Semaphore, synchronized).
Deadlock — een situatie waarin elke thread een bron vasthoudt en wacht op een bron die door een andere thread wordt vastgehouden. Geen enkele thread kan doorgaan. Voorwaarden voor optreden: wederzijdse uitsluiting, vasthouden en wachten, geen voorkoming, circulair wachten. Preventie: een uniforme volgorde voor het verkrijgen van vergrendelingen instellen, tryLock met time-out gebruiken, Lock-Free algoritmen toepassen (ConcurrentHashMap, CopyOnWriteArrayList).
Livelock — threads zijn niet geblokkeerd, maar geven constant bronnen aan elkaar door zonder nuttig werk te doen. Voorbeeld: twee personen komen elkaar tegen in een gang en beide stappen opzij, in dezelfde richting bewegend. Starvation — een thread krijgt geen toegang tot een bron omdat andere threads deze constant onderscheppen. Oplossing: eerlijke vergrendelingen (fair locks), thread-prioriteiten met voorzichtigheid.
Om multithreading-problemen te voorkomen, worden synchronisatieprimitieven gebruikt: Mutex (wederzijdse uitsluiting), Semaphore (beperking van het aantal gelijktijdige toegangen), Lock (interface met tryLock), Synchronized (JVM-niveau vergrendeling), @MainActor (Swift — garandeert uitvoering op de hoofdthread). Op Android is ook ThreadPool beschikbaar via Executors.newFixedThreadPool, newCachedThreadPool. Handmatig poolbeheer is echter het voorrecht van legacy-projecten; in nieuwe projecten kunt u beter coroutines gebruiken.
| Hulpmiddel | Platform | Type | Kenmerken |
|---|---|---|---|
| DispatchQueue (GCD) | iOS | Takenwachtrij | Serieel/Gelijktijdig, QoS-prioriteiten, Thread Pool beheerd door systeem |
| OperationQueue | iOS | Bewerkingenwachtrij | Afhankelijkheden, annulering, maxConcurrentOperationCount |
| Coroutines + Flow | Android | Coroutines | Lichtgewicht, gestructureerde gelijktijdigheid, StateFlow, Channel |
| RxJava / RxKotlin | Android | Reactieve stroom | Observable, Schedulers, rijke set operatoren |
| Combine | iOS | Reactieve stroom | Publisher/Subscriber, SwiftUI-integratie |
| Async/Await + Task | iOS / Android | Asynchroon model | Lineaire code, @MainActor, gestructureerde gelijktijdigheid |
Veelgestelde vragen
Main Thread (UI-thread) is verantwoordelijk voor het renderen van de interface en het verwerken van aanrakingen. Background Thread voert achtergrondtaken uit — gegevens laden, berekeningen, netwerkwerk. Het blokkeren van de Main Thread veroorzaakt het bevriezen van de interface (ANR op Android, frozen UI op iOS).
Race Condition — een raceconditie wanneer twee threads tegelijkertijd toegang krijgen tot gedeelde gegevens en het resultaat afhangt van de uitvoervolgorde. Wordt voorkomen door synchronisatie: Mutex, Semaphore, Lock, Synchronized, @MainActor of atomaire bewerkingen.
Coroutines is de moderne standaard voor Android (JetBrains, ondersteund door Google). RxJava/RxKotlin is een reactieve benadering met een rijke set operatoren. Coroutines zijn eenvoudiger voor asynchrone aanroepen, RxJava is krachtiger voor complexe datastromen. Bij IT Sectr gebruiken we Coroutines + Flow voor nieuwe projecten.
Deadlock — een wederzijdse blokkering waarbij twee threads wachten op elkaars bronnen. Livelock — threads zijn niet geblokkeerd, maar geven constant bronnen door zonder nuttig werk te doen. Beide problemen worden opgelost door de juiste vergrendelingsvolgorde en time-outs.
DispatchQueue is een abstractie van Grand Central Dispatch (GCD) voor threadbeheer. Main Queue voert taken uit op de hoofdthread, Global Queues — op achtergrondthreads. Serial Queue garandeert sequentiële uitvoering, Concurrent Queue — parallelle. In moderne projecten wordt GCD vaak vervangen door Async/Await en Task.
Samenvatting
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.