Każda aplikacja mobilna wykonuje wiele zadań jednocześnie: ładuje dane z sieci, przetwarza dotknięcia użytkownika, animuje interfejs i zapisuje pliki. Jeśli cały ten kod działa w jednym wątku, aplikacja zawiesza się przy każdym opóźnieniu sieciowym. Wielowątkowość (multithreading) i współbieżność to kluczowe koncepcje, które pozwalają aplikacji pozostać responsywną i wydajną. W tym artykule omówimy wszystkie główne narzędzia: od Main Thread i RunLoop po korutyny Kotlin i Combine na iOS. Materiał opiera się na oficjalnej dokumentacji Apple GCD.
Najważniejsze punkty
Wielowątkowość to zdolność aplikacji do wykonywania wielu fragmentów kodu jednocześnie. Każdy fragment jest wykonywany w osobnym wątku (Thread) — lekkim procesie z własnym stosem wywołań. W rozwoju mobilnym wątki dzielą się na dwie kategorie: Main Thread (wątek UI) i Background Threads (wątki tła).
System operacyjny sam zarządza dystrybucją wątków między rdzeniami procesora. Nowoczesne urządzenia mają 6–8 rdzeni, więc równoległe wykonywanie może rzeczywiście przyspieszyć pracę. Jednak tworzenie wątków jest kosztowną operacją, dlatego bezpośrednia praca z Thread nie jest zalecana. Zamiast tego używa się abstrakcji wyższego poziomu: DispatchQueue, OperationQueue, CoroutineDispatcher.
Współbieżność (Concurrency) to szersze pojęcie niż wielowątkowość. Współbieżność oznacza, że zadania mogą być wykonywane „jednocześnie” nawet na jednym rdzeniu poprzez przełączanie kontekstu. Asynchroniczność (Async/Await) to model programowania, w którym zadanie nie blokuje wątku, ale zwraca kontrolę podczas oczekiwania na wynik. Nowoczesne języki (Kotlin, Swift, Dart) mają wbudowane wsparcie dla Async/Await.
W IT Sectr przykładamy szczególną wagę do prawidłowej architektury wielowątkowości na starcie projektu. Błędy popełnione na wczesnym etapie prowadzą do trudnych do wykrycia błędów: wyścigów danych, zakleszczeń i niestabilności aplikacji pod obciążeniem. Każdy nasz projekt przechodzi przegląd architektury współbieżności na etapie planowania.
Main Thread (główny wątek) — jedyny wątek w aplikacji mobilnej, który ma dostęp do UI. Na Androidzie nazywa się UI Thread, na iOS — Main Thread. Wszystkie operacje interfejsu — zmiana tekstu, animacje, przetwarzanie dotknięć — są wykonywane tylko na Main Thread. Jeśli na głównym wątku wykonywana jest ciężka operacja (ładowanie pliku, parsowanie JSON), interfejs przestaje reagować. Na Androidzie prowadzi to do ANR (Application Not Responding), na iOS — do „zamrożenia” ekranu.
Background Threads (wątki tła) są przeznaczone do wszystkiego, co nie dotyczy UI: żądania sieciowe, operacje na bazie danych, przetwarzanie obrazów, kryptografia. Po zakończeniu pracy wynik jest przekazywany do Main Thread w celu wyświetlenia. Każda platforma zapewnia własne narzędzia do przełączania między wątkami: DispatchQueue.main.async w iOS, runOnUiThread lub withContext(Dispatchers.Main) w Androidzie.
RunLoop — pętla przetwarzania zdarzeń na głównym wątku iOS. RunLoop oczekuje zdarzeń (dotknięcia, timery, powiadomienia) i rozsyła je do odpowiednich handlerów. Na Androidzie odpowiednikiem jest Looper, powiązany z każdym Main Thread. Main Looper w nieskończoność wyciąga wiadomości z kolejki i przekazuje je do Handlera w celu przetworzenia. Zrozumienie RunLoop i Loopera pomaga uniknąć wycieków pamięci i „jąkania się” interfejsu.
Grand Central Dispatch (GCD) — biblioteka Apple do zarządzania wielowątkowością na poziomie języka C. GCD działa z DispatchQueue — kolejkami zadań. Deweloper nie tworzy wątków ręcznie; GCD zarządza pulą wątków (Thread Pool), rozdzielając zadania między dostępne rdzenie procesora. DispatchQueue są dwojakiego rodzaju: Serial Queue (kolejka szeregowa — zadania wykonywane są jedno po drugim) i Concurrent Queue (kolejka współbieżna — zadania mogą być wykonywane jednocześnie).
Main DispatchQueue — kolejka szeregowa związana z głównym wątkiem. Global Queues — kolejki współbieżne z różnymi priorytetami (QoS — Quality of Service): userInteractive, userInitiated, utility, background. Wybór odpowiedniego QoS jest krytyczny dla wydajności: .userInteractive — dla zadań wpływających na UI (animacje, renderowanie); .background — dla zadań niewrażliwych na czas (synchronizacja, czyszczenie pamięci podręcznej).
OperationQueue — abstrakcja nad GCD z dodatkowymi możliwościami: anulowanie zadań, ustawianie zależności między operacjami, kontrola maksymalnej liczby równoczesnych operacji. Operacje to obiekty klasy Operation (lub BlockOperation). Przykład: jeśli trzeba załadować obraz, następnie zastosować filtr i dopiero potem go wyświetlić — OperationQueue z zależnościami poradzi sobie idealnie. W GCD trzeba by ręcznie synchronizować te kroki za pomocą DispatchGroup lub semafora.
Async/Await w Swift 5.5+ — nowoczesna alternatywa dla GCD. Słowa kluczowe async i await czynią kod asynchroniczny liniowym i czytelnym. Funkcje są oznaczane jako async, a wywołania są oczekiwane przez await. System sam zarządza przełączaniem kontekstu: domyślnie funkcja async jest wykonywana w wątku tła, podczas gdy aktualizacja UI jest wykonywana na MainActor. @MainActor — atrybut gwarantujący wykonanie kodu na głównym wątku.
Coroutines (korutyny) — lekkie wątki dla Kotlin opracowane przez JetBrains. W przeciwieństwie do zwykłych wątków, korutyny nie są związane z konkretnym Thread. Tysiące korutyn może działać na kilku wątkach bez znaczącego narzutu. CoroutineScope zarządza cyklem życia korutyn: viewModelScope jest powiązany z ViewModel, lifecycleScope — z Activity/Fragment. Gdy zakres jest niszczony, wszystkie korutyny potomne są automatycznie anulowane.
Dispatchers określają, na której puli wątków wykonywana jest korutyna: Dispatchers.Main — wątek UI; Dispatchers.IO — dla żądań sieciowych i operacji dyskowych; Dispatchers.Default — dla obliczeń intensywnie korzystających z CPU. Do przełączania dispatchera używa się withContext. Korutyny obsługują współbieżność strukturalną (structured concurrency): każda korutyna ma rodzica, a po anulowaniu rodzica wszystkie korutyny potomne są anulowane. Zapobiega to wyciekom pamięci i wiszącym zadaniom.
Flow — zimny asynchroniczny strumień danych z biblioteki korutyn. Flow emituje wartości sekwencyjnie: (1) producent generuje dane, (2) operatory przekształcają strumień, (3) kolektor konsumuje wynik. W przeciwieństwie do LiveData, Flow obsługuje złożone łańcuchy operatorów (map, filter, flatMapConcat, catch) i jest całkowicie bezpieczny wątkowo. StateFlow i SharedFlow — gorące odmiany Flow, idealne do stanu UI i zdarzeń jednorazowych (Snackbar, nawigacja).
Channel — kolejna abstrakcja korutyn do przekazywania danych między korutynami. Channel działa jak kolejka: jeden nadawca (send) i jeden lub więcej odbiorców (receive). Kanały buforowane (Channel(UNLIMITED), Channel(BUFFERED)) pozwalają skonfigurować zachowanie przy przepełnieniu. Channel jest często używany z Flow do łączenia API opartych na callbackach z korutynami: callbackFlow { … }.
W IT Sectr aktywnie używamy korutyn i Flow we wszystkich projektach Android. Pozwala to pisać kod asynchroniczny, który wygląda jak synchroniczny, jest łatwy do testowania (runTest, TestDispatcher) i nie wymaga ręcznego zarządzania wątkami. Przykład prostej korutyny z ładowaniem danych:
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()
}
}
}
Programowanie reaktywne — paradygmat, w którym dane rozprzestrzeniają się jako asynchroniczne strumienie (Observable, Publisher). RxJava/RxKotlin — najpopularniejsza implementacja dla Androida, przeniesiona z .NET Rx. RxSwift — podobna biblioteka dla iOS. Główne komponenty: Observable (źródło zdarzeń), Observer (subskrybent), Scheduler (zarządzanie wątkami), Operators (transformacja strumienia).
Combine — framework Apple do programowania reaktywnego wprowadzony w iOS 13. Combine używa protokołów Publisher (wydawca) i Subscriber (subskrybent). W przeciwieństwie do RxSwift, Combine jest wbudowany w SDK i ściśle zintegrowany z SwiftUI. Operatory w Combine: map, filter, combineLatest, zip, debounce, throttle — obejmują większość scenariuszy: od wiązania danych z UI po debounce zapytania wyszukiwania.
Future i Promise — wzorce do pracy z pojedynczym wynikiem asynchronicznym. Future reprezentuje wartość, która będzie dostępna później. Promise to obietnica dostarczenia wartości. W Rx jest to Single (jedna pomyślna odpowiedź lub błąd), w Combine — Future Publisher. W praktyce Future/Promise są wygodne dla pojedynczych zapytań do API, a Observable/Publisher — dla ciągłych strumieni (geolokalizacja, wprowadzanie tekstu).
Callback i Delegate — klasyczne wzorce dla operacji asynchronicznych. Callback — funkcja przekazana jako argument i wywoływana po zakończeniu operacji. Delegate — obiekt implementujący protokół z metodami obsługi zdarzeń. Wada: „callback hell” (zagnieżdżone callbacki) i złożoność obsługi błędów. NotificationCenter (iOS) i EventBus (Android) — mechanizmy rozgłaszania zdarzeń, przydatne do luźno powiązanej komunikacji, ale prowadzące do ukrytych zależności.
Wielowątkowość otwiera drzwi do wysokiej wydajności, ale jednocześnie stwarza ryzyko trudnych do wykrycia błędów. Najczęstsze: Race Condition (stan wyścigu), Deadlock (zakleszczenie), Livelock (zakleszczenie aktywne) i Starvation (zagłodzenie wątku). Zrozumienie tych problemów to obowiązkowa umiejętność każdego programisty mobilnego.
Race Condition występuje, gdy dwa lub więcej wątków jednocześnie odczytuje i zapisuje te same dane bez synchronizacji. Wynik zależy od tego, który wątek wykona się pierwszy. Klasyczny przykład: dwa wątki inkrementują licznik. Operacja „odczytaj → zwiększ → zapisz” nie jest atomowa, więc przy jednoczesnym wykonaniu jeden przyrost „ginie”. Rozwiązanie — użycie operacji atomowych (AtomicInteger, AtomicReference) lub blokad (Mutex, Semaphore, synchronized).
Deadlock — sytuacja, w której każdy wątek trzyma zasób i czeka na zasób przechwycony przez inny wątek. Żaden wątek nie może kontynuować. Warunki wystąpienia: wzajemne wykluczanie, trzymanie i oczekiwanie, brak wywłaszczania, oczekiwanie cykliczne. Zapobieganie: ustalenie jednolitej kolejności przechwytywania blokad, użycie tryLock z limitem czasu, zastosowanie algorytmów Lock-Free (ConcurrentHashMap, CopyOnWriteArrayList).
Livelock — wątki nie są zablokowane, ale ciągle „przekazują” sobie zasoby, nie wykonując użytecznej pracy. Przykład: dwie osoby spotykają się na korytarzu i obie ustępują drogi, poruszając się w tym samym kierunku. Starvation — wątek nie uzyskuje dostępu do zasobu, ponieważ inne wątki go stale przechwytują. Rozwiązanie: uczciwe blokady (fair locks), ostrożne priorytety wątków.
Aby zapobiegać problemom wielowątkowości, używa się prymitywów synchronizacji: Mutex (wzajemne wykluczanie), Semaphore (ograniczenie liczby jednoczesnych dostępów), Lock (interfejs z tryLock), Synchronized (blokada na poziomie JVM), @MainActor (Swift — gwarancja wykonania na głównym wątku). Na Androidzie dostępny jest również ThreadPool (pula wątków) przez Executors.newFixedThreadPool, newCachedThreadPool. Jednak ręczne zarządzanie pulami to domena projektów legacy; w nowych projektach lepiej używać korutyn.
| Narzędzie | Platforma | Typ | Cechy |
|---|---|---|---|
| DispatchQueue (GCD) | iOS | Kolejka zadań | Szeregowa/Współbieżna, priorytety QoS, pula wątków zarządzana przez system |
| OperationQueue | iOS | Kolejka operacji | Zależności, anulowanie, maxConcurrentOperationCount |
| Coroutines + Flow | Android | Korutyny | Lekkie, współbieżność strukturalna, StateFlow, Channel |
| RxJava / RxKotlin | Android | Strumień reaktywny | Observable, Schedulers, bogaty zestaw operatorów |
| Combine | iOS | Strumień reaktywny | Publisher/Subscriber, integracja z SwiftUI |
| Async/Await + Task | iOS / Android | Model asynchroniczny | Liniowy kod, @MainActor, współbieżność strukturalna |
Często zadawane pytania
Main Thread (wątek UI) odpowiada za renderowanie interfejsu i przetwarzanie dotknięć. Background Thread wykonuje zadania w tle — ładowanie danych, obliczenia, pracę sieciową. Blokowanie Main Thread powoduje zawieszenie interfejsu (ANR na Androidzie, frozen UI na iOS).
Race Condition — stan wyścigu, gdy dwa wątki jednocześnie uzyskują dostęp do wspólnych danych, a wynik zależy od kolejności wykonania. Unika się go poprzez synchronizację: Mutex, Semaphore, Lock, Synchronized, @MainActor lub operacje atomowe.
Coroutines to nowoczesny standard dla Androida (JetBrains, wspierany przez Google). RxJava/RxKotlin to podejście reaktywne z bogatym zestawem operatorów. Coroutines są prostsze dla wywołań asynchronicznych, RxJava jest potężniejszy dla złożonych strumieni danych. W IT Sectr używamy Coroutines + Flow dla nowych projektów.
Deadlock — wzajemne blokowanie, gdy dwa wątki czekają na zasoby siebie nawzajem. Livelock — wątki nie są zablokowane, ale ciągle przekazują sobie zasoby bez wykonywania użytecznej pracy. Oba problemy rozwiązuje się odpowiednią kolejnością blokad i limitami czasu.
DispatchQueue to abstrakcja Grand Central Dispatch (GCD) do zarządzania wątkami. Main Queue wykonuje zadania na głównym wątku, Global Queues — na wątkach tła. Serial Queue gwarantuje sekwencyjne wykonanie, Concurrent Queue — równoległe. W nowoczesnych projektach GCD jest często zastępowane przez Async/Await i Task.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.