Thread — unitatea de bază a timpului de procesor, care are propriul stiv și se execută independent de alte fire de execuție. În dezvoltarea mobilă, firele sunt folosite pentru execuția paralelă a sarcinilor, astfel încât interfața să rămână receptivă în timpul operațiunilor lungi. Android suportă java.lang.Thread, Executors și Kotlin Coroutines, iOS — Thread (Objective-C), GCD și OperationQueue. Potrivit Android Thread Documentation, crearea unui fir nativ necesită alocarea a ~1 MB pentru stivă de către sistemul de operare.
Puncte cheie
Thread (fir de execuție) — o secvență independentă de instrucțiuni pe care sistemul de operare o poate planifica pe nucleul CPU. Fiecare proces (aplicație) conține cel puțin un fir — Main Thread. Firele suplimentare sunt create pentru execuția paralelă a sarcinilor. Fiecare fir are propriul stiv de program (cu variabile locale), contorul de program (PC) și registre. Memoria heap este partajată de toate firele procesului.
În sistemele de operare mobile, firele sunt planificate prin multitasking preemptiv (preemptive multitasking): sistemul de operare poate întrerupe execuția unui fir în orice moment și poate transmite controlul altuia (context switch). Comutarea de context este o operațiune costisitoare (1–10 microsecunde), deoarece necesită salvarea/restaurarea registrelor CPU, actualizarea TLB și golirea cache-urilor. Tocmai de aceea un număr excesiv de fire (sute și mii) degradează performanța — sistemul de operare petrece mai mult timp comutând decât executând.
Fir și proces sunt concepte diferite. Procesul este o instanță a aplicației cu memorie virtuală dedicată. Firul din interiorul procesului împarte această memorie cu alte fire. În Android, fiecare component al aplicației (Activity, Service, BroadcastReceiver) funcționează într-un singur proces, dar poate fi executat în fire diferite. Aplicația iOS este, de asemenea, un singur proces cu posibilitatea de a crea fire suplimentare prin GCD sau Thread.
Fiecare fir în Java/Kotlin (Android) și NSThread (iOS) trece prin cinci stări: New (creat), Runnable (gata de execuție), Running (se execută pe CPU), Blocked/Waiting (așteaptă o resursă sau o notificare), Terminated (terminat). Tranzițiile între stări sunt gestionate de planificatorul sistemului de operare și de primitivele de sincronizare. Dezvoltatorul poate influența prioritatea firului (Thread.setPriority()) și starea acestuia (sleep, join, interrupt).
În Android, firul trece în starea Blocked la încercarea de a captura un monitor ocupat (synchronized), la apelul Object.wait() sau Thread.sleep(). În iOS — la apelul NSCondition.wait(), pthread_cond_wait() sau dispatch_semaphore_wait(). În starea Blocked, firul nu consumă CPU, dar ocupă memorie (stiv). Firul poate fi întrerupt (interrupted) dintr-un alt fir, primind InterruptedException (Java) sau verificând isCancelled (Kotlin Coroutines).
| Stare | Descriere | Metodă de tranziție |
|---|---|---|
| New | Firul este creat, dar nu a fost pornit | Constructorul Thread() |
| Runnable | Firul este gata de execuție, așteaptă CPU | thread.start() |
| Running | Firul se execută pe nucleul CPU | Planificatorul sistemului de operare |
| Blocked/Waiting | Firul așteaptă o resursă, un monitor sau o notificare | synchronized, wait(), sleep() |
| Terminated | Firul a terminat run() sau a fost întrerupt | run() terminat, interrupt() |
Context switch (comutarea de context) — operațiunea în care sistemul de operare salvează starea firului curent (registre, PC, TLB) și încarcă starea salvată a altuia. În sistemele mobile (Linux + ART, XNU pentru iOS), context switch durează 1–10 microsecunde. Dacă firul execută o sarcină în 100 de microsecunde, iar context switch durează 5, atunci 5% din timp este irosit. Pentru a minimiza context switch, iOS folosește GCD cu work stealing, iar Android — pool-uri cu fixedThreadCount.
Android a evoluat de la java.lang.Thread de nivel scăzut la corutinele moderne. Fiecare nivel de abstractizare oferă mai multe posibilități cu costuri suplimentare mai mici. Thread este clasa de bază, dar crearea sa directă nu este recomandată: firul nou nu este gestionat de un pool, este greu de monitorizat și anulat. AsyncTask (deprecated din API 30) a fost un pas înainte, dar suferea de scurgeri de memorie și de gestionarea incomodă a configurațiilor.
HandlerThread — o subclasă specială a Thread cu Looper, care poate procesa o coadă de mesaje. Este folosit pentru execuția secvențială a sarcinilor pe un fir de fundal, de exemplu scrierea datelor în Room sau în fișiere. HandlerThread este creat prin apelul start(), după care prin Handler(handlerThread.looper) pot fi trimise mesaje și Runnable. Apelul handlerThread.quit() oprește Looper și termină firul.
// Android: Thread, HandlerThread și Executors
import android.os.Handler
import android.os.HandlerThread
import java.util.concurrent.Executors
class ThreadExample {
// 1. Crearea directă a Thread (nu este recomandată)
fun directThread() {
val thread = Thread(Runnable {
Thread.sleep(1000)
print("Firul direct executat")
})
thread.start()
}
// 2. HandlerThread pentru sarcini secvențiale de fundal
fun handlerThreadExample() {
val handlerThread = HandlerThread("BackgroundQueue")
handlerThread.start()
val handler = Handler(handlerThread.looper)
handler.post {
// Execuție secvențială pe firul de fundal
Thread.sleep(500)
print("HandlerThread: sarcina executată")
}
// Oprirea firului (se execută când sarcinile sunt terminate)
handlerThread.quitSafely()
}
// 3. Executors — pool de fire
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 — standardul modern
suspend fun coroutineExample() = kotlinx.coroutines.withContext(
kotlinx.coroutines.Dispatchers.Default
) {
print("Coroutine on thread: ${Thread.currentThread().getName()}")
}
}
Exemplul ThreadExample arată toate cele patru niveluri de abstractizare a firelor în Android. Crearea directă a Thread este cea mai de nivel scăzut și ineficientă abordare. HandlerThread este util pentru sarcini secvențiale în fundal. Executors.newFixedThreadPool(4) creează un pool de 4 fire pentru execuția paralelă a până la 10 sarcini. Kotlin Coroutines cu Dispatchers.Default — o metodă modernă, eficientă și sigură.
HandlerThread — o subclasă specializată a Thread cu Looper încorporat și coadă de mesaje. Este creat prin apelul start(), după care prin Handler(handlerThread.looper) pot fi trimise Runnable și mesaje. HandlerThread execută sarcinile strict secvențial — sarcina următoare nu începe până la finalizarea celei anterioare. Acest lucru este convenabil pentru scrierea datelor în Room sau în fișiere, unde ordinea operațiunilor este critică. Apelul quitSafely() oprește Looper după finalizarea sarcinii curente.
iOS oferă, de asemenea, trei niveluri de lucru cu firele. Thread (Thread în Swift, NSThread în Objective-C) — API de nivel scăzut care creează direct un fir nativ. GCD (Grand Central Dispatch) prin DispatchQueue — principalul instrument pentru dezvoltatorii iOS, care gestionează automat pool-ul de fire. OperationQueue — o abstractizare de nivel înalt peste GCD cu suport pentru dependențe, priorități și anulare.
Utilizarea directă a Thread în dezvoltarea iOS modernă este extrem de rară — GCD oferă toate posibilitățile necesare cu gestionare automată a memoriei și firelor. Thread este folosit doar pentru cazuri specifice: configurarea thread-local storage (threadDictionary), crearea unui RunLoop pentru un fir de fundal sau integrarea cu biblioteci C care așteaptă pthread_t.
import Foundation
class ThreadManager {
// 1. Thread (de nivel scăzut)
func createThread() {
let thread = Thread {
// Codul se execută pe un fir nou
print("Current thread: \(Thread.current)")
}
thread.name = "com.app.worker"
thread.qualityOfService = .utility
thread.start()
}
// 2. GCD — DispatchQueue
func gcdExample() {
// Coadă paralelă
let queue = DispatchQueue(label: "com.app.concurrent",
qos: .utility,
attributes: .concurrent)
queue.async {
print("Sarcină asincronă GCD")
}
// Barrier pentru sincronizarea scrierii
queue.async(flags: .barrier) {
// Acces exclusiv în timpul scrierii
print("Barrier write: acces exclusiv")
}
}
// 3. OperationQueue cu dependențe
func operationQueueExample() {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2
queue.qualityOfService = .background
let download = BlockOperation {
print("Se descarcă...")
}
let process = BlockOperation {
print("Se procesează...")
}
let save = BlockOperation {
print("Se salvează...")
}
// Dependențe: download -> process -> save
process.addDependency(download)
save.addDependency(process)
queue.addOperations([download, process, save], waitUntilFinished: false)
}
}
// Colecție thread-safe prin 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)
}
}
}
Clasa ThreadSafeArray demonstrează modelul Concurrent Read / Exclusive Write prin GCD barrier. Citirea prin queue.sync{} se execută paralel din mai multe fire. Scrierea prin queue.async(flags: .barrier) blochează toate celelalte operațiuni (atât citirea, cât și scrierea) până la finalizarea scrierii. Acest lucru este mai eficient decât blocurile synchronized, deoarece nu blochează cititorii cât timp nu există scriere.
Utilizarea directă a Thread în iOS este justificată în trei cazuri: pentru thread-local storage (Thread.current.threadDictionary) — stocarea datelor legate de fir; pentru crearea unui RunLoop special pe un fir de fundal cu performSelector:onThread:; pentru integrarea cu biblioteci C/C++ care așteaptă pthread_t. În toate celelalte cazuri, GCD prin DispatchQueue este preferat — gestionează automat pool-ul de fire și consumul de energie.
Race condition (condiția de cursă) apare atunci când două sau mai multe fire accesează simultan date partajate și cel puțin unul dintre ele execută o scriere. Rezultatul depinde de ordinea execuției (timing) și este imprevizibil. Pentru prevenirea race condition se folosesc primitive de sincronizare. În dezvoltarea mobilă sunt disponibile blocări (synchronized, NSLock), operații atomice (AtomicInteger, proprietăți atomic în iOS) și cozi (serial queue).
Alegerea primitivei depinde de scenariu. Pentru contoare și flag-uri simple sunt suficiente operațiile atomice (AtomicInteger, atomic property). Pentru secțiuni critice cu mai multe operații — blocările (synchronized, NSLock). Pentru structuri de date complexe — serial DispatchQueue sau GCD barrier. Blocările sunt mai ușor de înțeles, dar sunt susceptibile la deadlock și livelock. Cozile sunt mai complexe, dar mai sigure.
// Sincronizare în Android/Kotlin
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
class Counter {
// 1. AtomicInteger — pentru contoare simple
private val atomicCount = AtomicInteger(0)
fun incrementAtomic() = atomicCount.incrementAndGet()
// 2. synchronized — pentru secțiuni critice
@Synchronized
fun synchronizedOperation() {
// Doar un fir pe rând
doWork()
}
// 3. Mutex din corutine — suspend-safe
private val mutex = Mutex()
suspend fun mutexOperation() {
mutex.withLock {
// protected code — thread-safe
doWork()
}
}
private fun doWork() { /* critical section */ }
}
// Exemplu deadlock: A blochează B, B blochează 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 demonstrează trei abordări de sincronizare. AtomicInteger.incrementAndGet() — operație atomică fără blocări (CAS). @Synchronized — monitorul încorporat al Java, blochează întregul obiect. Mutex.withLock — mutex de corutină, suspendă corutina în loc să blocheze firul (mai eficient). DeadlockExample arată un deadlock clasic: două fire capturează blocările în ordine diferită.
Thread Pool (pool de fire) — un set de fire create în avans care sunt reutilizate pentru execuția sarcinilor. În loc să creeze un fir nou pentru fiecare sarcină (costisitor), pool-ul ia un fir liber din pool. Dacă nu există fire libere, sarcina este pusă în coadă. Pool-ul gestionează automat dimensiunea: firele noi sunt create la vârfuri de încărcare, firele inactive sunt terminate. Acest lucru reduce costurile de creare a firelor de zeci de ori.
În Android, Executors.newFixedThreadPool(4) creează un pool de 4 fire. Dacă vin simultan 10 sarcini, 4 încep imediat, iar 6 așteaptă în coadă. Executors.newCachedThreadPool() creează fire după necesitate (fără limită) și termină firele inactive după 60 de secunde. Pentru iOS, GCD oferă automat pool-uri de cozi globale, a căror dimensiune corespunde numărului de nuclee CPU și încărcării curente.
În Kotlin Coroutines, pool-urile de fire sunt ascunse în interiorul dispatcher-ilor. Dispatchers.Default folosește un pool de dimensiune egală cu numărul de nuclee CPU (minimum 2). Dispatchers.IO — 64 de fire (suficiente pentru sute de sarcini IO-bound, deoarece majoritatea vor aștepta intrare-ieșire fără a ocupa CPU). Fiecare dispatcher scala automat pool-ul în funcție de încărcare, economisind energia bateriei în timpul inactivității.
Întrebări frecvente
Thread — unitatea de bază a execuției codului într-o aplicație. Fiecare proces poate avea multe fire care partajează memoria, dar au propriul stiv. În dezvoltarea mobilă, firele sunt folosite pentru execuția paralelă a sarcinilor fără a bloca UI. Android folosește Thread, Executors, HandlerThread și Coroutines. iOS folosește Thread, GCD (DispatchQueue) și OperationQueue.
Crearea Thread necesită alocarea a ~1 MB pentru stivă în Android și ~512 KB în iOS — este o operațiune costisitoare. Pentru 1000 de sarcini, crearea directă a 1000 de fire va necesita ~1 GB doar pentru stive, plus costuri suplimentare pentru context switch. În loc de Thread, folosiți pool-uri (Executors, GCD) sau corutine — ele reutilizează firele, reducând costurile de zeci de ori.
Race condition — comportament imprevizibil la accesul simultan al mai multor fire la date partajate cu scriere. Poate fi evitată în trei moduri: folosirea tipurilor atomice (AtomicInteger), a blocărilor (synchronized, NSLock) sau serializarea accesului printr-o coadă (DispatchQueue serial, Actor în Kotlin). Cea mai bună practică — minimizarea stării mutable partajate și folosirea immutability.
Thread — un obiect de sistem nativ care ocupă ~1 MB de stivă și este legat de nucleul sistemului de operare. Corutina — o unitate de execuție ușoară în Kotlin care nu este legată de un fir specific și poate fi suspendată (suspend) fără blocare. Un singur fir poate executa mii de corutine. Corutinele sunt mai eficiente din punct de vedere al memoriei și permit scrierea codului asincron fără callbacks.
Deadlock se manifestă ca o înghețare completă a aplicației fără ANR. În Android folosiți Thread.getAllStackTraces() pentru a face dump-ul stivelor tuturor firelor — două fire vor aștepta blocările unul altuia. În iOS — Thread.callStackSymbols. Instrumente: Android Studio Profiler (tab-ul Threads), Instruments (iOS, Thread State View). Prevenire: capturați blocările într-o ordine fixă, folosiți tryLock cu timeout.
Concluzii
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și