Fiecare aplicație mobilă efectuează multe sarcini simultan: încarcă date din rețea, procesează atingerile utilizatorului, animează interfața și salvează fișiere. Dacă tot acest cod rulează într-un singur fir, aplicația îngheață la orice întârziere de rețea. Multithreading și concurența sunt concepte cheie care permit aplicației să rămână receptivă și eficientă. În acest articol vom acoperi toate instrumentele principale: de la Main Thread și RunLoop până la corutinele Kotlin și Combine pe iOS. Materialul se bazează pe documentația oficială Apple GCD.
Puncte Cheie
Multithreading este capacitatea unei aplicații de a executa mai multe fragmente de cod simultan. Fiecare fragment rulează într-un fir (Thread) separat — un proces ușor cu propria stivă de apeluri. În dezvoltarea mobilă, firele se împart în două categorii: Main Thread (fir UI) și Background Threads (fire de fundal).
Sistemul de operare gestionează el însuși distribuirea firelor pe nucleele procesorului. Dispozitivele moderne au 6–8 nuclee, deci execuția paralelă poate accelera lucrul. Cu toate acestea, crearea de fire este o operație costisitoare, deci nu se recomandă lucrul direct cu Thread. În schimb, se folosesc abstractizări de nivel superior: DispatchQueue, OperationQueue, CoroutineDispatcher.
Concurența (Concurrency) este un concept mai larg decât multithreading. Concurența înseamnă că sarcinile pot fi executate „simultan” chiar și pe un singur nucleu prin schimbarea contextului. Asincronia (Async/Await) este un model de programare în care o sarcină nu blochează un fir, ci returnează controlul în timp ce așteaptă un rezultat. Limbajele moderne (Kotlin, Swift, Dart) au suport încorporat pentru Async/Await.
La IT Sectr, acordăm o atenție deosebită arhitecturii corecte de multithreading la începutul proiectului. Greșelile făcute într-un stadiu incipient duc la erori dificil de depistat: curse de date, blocaje mutuale și instabilitate a aplicației sub sarcină. Fiecare proiect al nostru trece printr-o revizuire a arhitecturii de concurență în faza de planificare.
Main Thread (firul principal) — singurul fir dintr-o aplicație mobilă care are acces la UI. Pe Android se numește UI Thread, pe iOS — Main Thread. Toate operațiile cu interfața — modificarea textului, animații, procesarea atingerilor — se efectuează doar pe Main Thread. Dacă pe firul principal se execută o operație grea (încărcare fișier, parsare JSON), interfața nu mai răspunde. Pe Android aceasta duce la ANR (Application Not Responding), pe iOS — la „înghețarea” ecranului.
Background Threads (firele de fundal) sunt destinate pentru tot ce nu ține de UI: cereri de rețea, operații cu baza de date, procesare de imagini, criptografie. După finalizare, rezultatul este transmis la Main Thread pentru afișare. Fiecare platformă oferă propriile instrumente pentru comutarea între fire: DispatchQueue.main.async pe iOS, runOnUiThread sau withContext(Dispatchers.Main) pe Android.
RunLoop — bucla de procesare a evenimentelor pe firul principal iOS. RunLoop așteaptă evenimente (atingeri, temporizatoare, notificări) și le distribuie către gestionarii corespunzători. Pe Android echivalentul este Looper, asociat cu fiecare Main Thread. Main Looper extrage mesaje infinit din coadă și le transmite Handler-ului pentru procesare. Înțelegerea RunLoop și Looper ajută la evitarea scurgerilor de memorie și a „bâlbâielii” interfeței.
Grand Central Dispatch (GCD) — biblioteca Apple pentru gestionarea multithreading-ului la nivelul limbajului C. GCD lucrează cu DispatchQueue — cozi de sarcini. Dezvoltatorul nu creează fire manual; GCD gestionează un pool de fire (Thread Pool), distribuind sarcinile pe nucleele disponibile ale procesorului. DispatchQueue sunt de două tipuri: Serial Queue (coadă serială — sarcinile se execută una după alta) și Concurrent Queue (coadă concurentă — sarcinile se pot executa simultan).
Main DispatchQueue — o coadă serială legată de firul principal. Global Queues — cozi concurente cu priorități diferite (QoS — Quality of Service): userInteractive, userInitiated, utility, background. Alegerea QoS corecte este esențială pentru performanță: .userInteractive — pentru sarcini care afectează UI (animații, randare); .background — pentru sarcini necritice ca timp (sincronizare, curățare cache).
OperationQueue — o abstractizare peste GCD cu capacități suplimentare: anularea sarcinilor, stabilirea dependențelor între operații, controlul numărului maxim de operații concurente. Operațiile sunt obiecte ale clasei Operation (sau BlockOperation). Exemplu: dacă trebuie să încărcați o imagine, apoi să aplicați un filtru și abia apoi să o afișați — OperationQueue cu dependențe se descurcă perfect. În GCD ar trebui să sincronizați manual acești pași folosind DispatchGroup sau semafor.
Async/Await în Swift 5.5+ — o alternativă modernă la GCD. Cuvintele cheie async și await fac codul asincron liniar și lizibil. Funcțiile sunt marcate ca async, iar apelurile sunt așteptate prin await. Sistemul însuși gestionează comutarea contextului: în mod implicit, o funcție async se execută pe un fir de fundal, în timp ce actualizarea UI se execută pe MainActor. @MainActor — un atribut care garantează execuția codului pe firul principal.
Coroutines (corutinele) — fire ușoare pentru Kotlin dezvoltate de JetBrains. Spre deosebire de firele obișnuite, corutinele nu sunt legate de un anumit Thread. Mii de corutine pot rula pe mai multe fire fără un overhead semnificativ. CoroutineScope gestionează ciclul de viață al corutinelor: viewModelScope este legat de ViewModel, lifecycleScope — de Activity/Fragment. Când domeniul este distrus, toate corutinele copil sunt anulate automat.
Dispatchers determină pe ce pool de fire se execută corutina: Dispatchers.Main — fir UI; Dispatchers.IO — pentru cereri de rețea și operații pe disc; Dispatchers.Default — pentru calcule intensive CPU. Pentru a schimba dispatcherul se folosește withContext. Corutinele suportă concurența structurată: fiecare corutină are un părinte, iar când părintele este anulat, toate corutinele copil sunt anulate. Aceasta previne scurgerile de memorie și sarcinile suspendate.
Flow — un flux de date asincron rece din biblioteca de corutine. Flow emite valori secvențial: (1) producătorul generează date, (2) operatorii transformă fluxul, (3) colectorul consumă rezultatul. Spre deosebire de LiveData, Flow suportă lanțuri complexe de operatori (map, filter, flatMapConcat, catch) și este complet sigur pentru fire. StateFlow și SharedFlow — variante fierbinți ale Flow, ideale pentru starea UI și evenimente unice (Snackbar, navigare).
Channel — o altă abstractizare de corutine pentru transmiterea datelor între corutine. Channel funcționează ca o coadă: un expeditor (send) și unul sau mai mulți destinatari (receive). Canalele cu buffer (Channel(UNLIMITED), Channel(BUFFERED)) permit configurarea comportamentului la debordare. Channel este adesea folosit cu Flow pentru a face punte între API-urile bazate pe callback și corutine: callbackFlow { … }.
La IT Sectr folosim activ corutinele și Flow în toate proiectele Android. Aceasta permite scrierea de cod asincron care arată sincron, este ușor de testat (runTest, TestDispatcher) și nu necesită gestionare manuală a firelor. Exemplu de corutină simplă cu încărcare de date:
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()
}
}
}
Programarea reactivă — un paradigm în care datele se propagă ca fluxuri asincrone (Observable, Publisher). RxJava/RxKotlin — cea mai populară implementare pentru Android, portată din .NET Rx. RxSwift — o bibliotecă similară pentru iOS. Componente principale: Observable (sursă de evenimente), Observer (abonat), Scheduler (gestionare fire), Operators (transformare flux).
Combine — un framework Apple pentru programare reactivă introdus în iOS 13. Combine folosește protocoalele Publisher (editor) și Subscriber (abonat). Spre deosebire de RxSwift, Combine este încorporat în SDK și strâns integrat cu SwiftUI. Operatori în Combine: map, filter, combineLatest, zip, debounce, throttle — acoperă majoritatea scenariilor: de la legarea datelor la UI până la debounce-ul interogării de căutare.
Future și Promise — modele pentru lucrul cu un singur rezultat asincron. Future reprezintă o valoare care va fi disponibilă mai târziu. Promise este o promisiune de a furniza o valoare. În Rx aceasta este Single (un răspuns reușit sau o eroare), în Combine — Future Publisher. În practică, Future/Promise sunt convenabile pentru cereri unice către API, în timp ce Observable/Publisher — pentru fluxuri continue (geolocalizare, introducere text).
Callback și Delegate — modele clasice pentru operații asincrone. Callback — o funcție transmisă ca argument și apelată la finalizarea operației. Delegate — un obiect care implementează un protocol cu metode de gestionare a evenimentelor. Dezavantaj: „callback hell” (callback-uri imbricate) și complexitatea gestionării erorilor. NotificationCenter (iOS) și EventBus (Android) — mecanisme de difuzare a evenimentelor, utile pentru comunicare slab cuplată, dar care duc la dependențe implicite.
Multithreading-ul deschide ușa către performanțe înalte, dar în același timp creează riscul unor erori dificil de depistat. Cele mai comune: Race Condition (condiție de cursă), Deadlock (blocare mutuală), Livelock (blocare activă) și Starvation (înfometarea firului). Înțelegerea acestor probleme este o abilitate esențială pentru orice dezvoltator mobil.
Race Condition apare atunci când două sau mai multe fire citesc și scriu simultan aceleași date fără sincronizare. Rezultatul depinde de care fir se execută primul. Exemplu clasic: două fire incrementează un contor. Operația „citește → incrementează → scrie” nu este atomică, deci atunci când este executată simultan, un increment se „pierde”. Soluția — utilizarea operațiilor atomice (AtomicInteger, AtomicReference) sau a blocărilor (Mutex, Semaphore, synchronized).
Deadlock — o situație în care fiecare fir deține o resursă și așteaptă o resursă deținută de un alt fir. Niciun fir nu poate continua. Condiții de apariție: excludere mutuală, deținere și așteptare, fără preempțiune, așteptare circulară. Prevenire: stabilirea unui singur ordin de achiziție a blocărilor, utilizarea tryLock cu timeout, aplicarea algoritmilor Lock-Free (ConcurrentHashMap, CopyOnWriteArrayList).
Livelock — firele nu sunt blocate, dar își „pasă” în mod constant resurse una alteia fără a efectua muncă utilă. Exemplu: două persoane se întâlnesc într-un coridor și ambele fac loc, mișcându-se în aceeași direcție. Starvation — un fir nu obține acces la o resursă deoarece alte fire o interceptează constant. Soluție: blocări echitabile (fair locks), priorități de fire cu prudență.
Pentru prevenirea problemelor de multithreading se folosesc primitive de sincronizare: Mutex (excludere mutuală), Semaphore (limitarea numărului de accesări simultane), Lock (interfață cu tryLock), Synchronized (blocare la nivel JVM), @MainActor (Swift — garanția execuției pe firul principal). Pe Android este disponibil și ThreadPool (pool de fire) prin Executors.newFixedThreadPool, newCachedThreadPool. Cu toate acestea, gestionarea manuală a pool-urilor este prerogativa proiectelor moștenite; în proiectele noi este mai bine să se folosească corutine.
| Instrument | Platformă | Tip | Caracteristici |
|---|---|---|---|
| DispatchQueue (GCD) | iOS | Coadă de sarcini | Serială/Concurentă, priorități QoS, Thread Pool gestionat de sistem |
| OperationQueue | iOS | Coadă de operații | Dependențe, anulare, maxConcurrentOperationCount |
| Coroutines + Flow | Android | Corutine | Ușoare, concurență structurată, StateFlow, Channel |
| RxJava / RxKotlin | Android | Flux reactiv | Observable, Schedulers, set bogat de operatori |
| Combine | iOS | Flux reactiv | Publisher/Subscriber, integrare SwiftUI |
| Async/Await + Task | iOS / Android | Model asincron | Cod liniar, @MainActor, concurență structurată |
Întrebări frecvente
Main Thread (fir UI) este responsabil pentru randarea interfeței și procesarea atingerilor. Background Thread execută sarcini în fundal — încărcare date, calcule, lucru în rețea. Blocarea Main Thread cauzează înghețarea interfeței (ANR pe Android, frozen UI pe iOS).
Race Condition — o condiție de cursă când două fire accesează simultan date partajate și rezultatul depinde de ordinea de execuție. Se evită prin sincronizare: Mutex, Semaphore, Lock, Synchronized, @MainActor sau operații atomice.
Coroutines este standardul modern pentru Android (JetBrains, acceptat de Google). RxJava/RxKotlin este o abordare reactivă cu un set bogat de operatori. Coroutines sunt mai simple pentru apeluri asincrone, RxJava este mai puternic pentru fluxuri de date complexe. La IT Sectr folosim Coroutines + Flow pentru proiecte noi.
Deadlock — o blocare mutuală în care două fire așteaptă resursele unul altuia. Livelock — firele nu sunt blocate, dar își transferă constant resurse fără a face muncă utilă. Ambele probleme se rezolvă prin ordinea corectă a blocărilor și timeout-uri.
DispatchQueue este o abstractizare a Grand Central Dispatch (GCD) pentru gestionarea firelor. Main Queue execută sarcini pe firul principal, Global Queues — pe fire de fundal. Serial Queue garantează execuția secvențială, Concurrent Queue — paralelă. În proiectele moderne, GCD este adesea înlocuit de Async/Await și Task.
Rezumat
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.