Thread to podstawowa jednostka czasu procesora, mająca własny stos i wykonująca się niezależnie od innych wątków. W tworzeniu aplikacji mobilnych wątki są wykorzystywane do równoległego wykonywania zadań, aby interfejs pozostał responsywny podczas długotrwałych operacji. Android obsługuje java.lang.Thread, Executors i Kotlin Coroutines, iOS — Thread (Objective-C), GCD i OperationQueue. Według Android Thread Documentation utworzenie natywnego wątku wymaga przydzielenia ~1 MB stosu przez system operacyjny.
Najważniejsze
Thread (wątek wykonawczy) to niezależna sekwencja instrukcji, którą system operacyjny może zaplanować na rdzeniu CPU. Każdy proces (aplikacja) zawiera co najmniej jeden wątek — Main Thread. Dodatkowe wątki są tworzone do równoległego wykonywania zadań. Każdy wątek ma własny stos programowy (z lokalnymi zmiennymi), licznik rozkazów (PC) i rejestry. Pamięć sterty (heap) jest wspólna dla wszystkich wątków procesu.
W mobilnych systemach operacyjnych wątki są planowane przez wywłaszczającą wielozadaniowość (preemptive multitasking): system operacyjny może przerwać wykonanie wątku w dowolnym momencie i przekazać sterowanie innemu (context switch). Przełączanie kontekstu to kosztowna operacja (1–10 mikrosekund), ponieważ wymaga zapisania i przywrócenia rejestrów CPU, zaktualizowania TLB oraz wyczyszczenia pamięci podręcznych. Dlatego nadmierna liczba wątków (setki i tysiące) pogarsza wydajność — system operacyjny poświęca więcej czasu na przełączanie niż na wykonywanie.
Wątek a proces to różne pojęcia. Proces to instancja aplikacji z wydzieloną pamięcią wirtualną. Wątek wewnątrz procesu dzieli tę pamięć z innymi wątkami. W Android każdy komponent aplikacji (Activity, Service, BroadcastReceiver) działa w jednym procesie, ale może wykonywać się w różnych wątkach. Aplikacja iOS również jest jednym procesem z możliwością tworzenia dodatkowych wątków przez GCD lub Thread.
Każdy wątek w Java/Kotlin (Android) i NSThread (iOS) przechodzi przez pięć stanów: New (utworzony), Runnable (gotowy do wykonania), Running (wykonywany na CPU), Blocked/Waiting (oczekuje na zasób lub powiadomienie), Terminated (zakończony). Przejścia między stanami są zarządzane przez planistę systemu operacyjnego i prymitywy synchronizacji. Programista może wpływać na priorytet wątku (Thread.setPriority()) i jego stan (sleep, join, interrupt).
W Android wątek przechodzi w stan Blocked przy próbie zajęcia zajętego monitora (synchronized), wywołaniu Object.wait() lub Thread.sleep(). W iOS — przy wywołaniu NSCondition.wait(), pthread_cond_wait() lub dispatch_semaphore_wait(). W stanie Blocked wątek nie zużywa CPU, ale zajmuje pamięć (stos). Wątek może być przerwany (interrupted) z innego wątku, otrzymując InterruptedException (Java) lub sprawdzając isCancelled (Kotlin Coroutines).
| Stan | Opis | Metoda przejścia |
|---|---|---|
| New | Wątek utworzony, ale nie uruchomiony | Konstruktor Thread() |
| Runnable | Wątek gotowy do wykonania, oczekuje na CPU | thread.start() |
| Running | Wątek wykonuje się na rdzeniu CPU | Planista systemu operacyjnego |
| Blocked/Waiting | Wątek oczekuje na zasób, monitor lub powiadomienie | synchronized, wait(), sleep() |
| Terminated | Wątek zakończył run() lub został przerwany | run() zakończony, interrupt() |
Context switch (przełączanie kontekstu) to operacja, w której system operacyjny zapisuje stan bieżącego wątku (rejestry, PC, TLB) i wczytuje zapisany stan innego. W systemach mobilnych (Linux + ART, XNU dla iOS) context switch trwa 1–10 mikrosekund. Jeśli wątek wykonuje zadanie w 100 mikrosekund, a context switch zajmuje 5, to 5% czasu jest tracone. Aby zminimalizować context switch, iOS używa GCD z work stealing, Android — puli z fixedThreadCount.
Android przeszedł ewolucję od niskopoziomowego java.lang.Thread do nowoczesnych korutyn. Każdy poziom abstrakcji daje więcej możliwości przy mniejszych kosztach ogólnych. Thread to klasa bazowa, ale jej bezpośrednie tworzenie nie jest zalecane: nowy wątek nie jest zarządzany przez pulę, trudno go monitorować i anulować. AsyncTask (deprecated od API 30) był krokiem naprzód, ale cierpiał na wycieki pamięci i niewygodną obsługę konfiguracji.
HandlerThread to specjalna podklasa Thread z Looper, która może przetwarzać kolejkę komunikatów. Jest używana do sekwencyjnego wykonywania zadań w wątku tła, na przykład zapisu danych do Room lub plików. HandlerThread jest tworzony wywołaniem start(), po czym przez Handler(handlerThread.looper) można wysyłać komunikaty i Runnable. Wywołanie handlerThread.quit() zatrzymuje Looper i kończy wątek.
// Android: Thread, HandlerThread i Executors
import android.os.Handler
import android.os.HandlerThread
import java.util.concurrent.Executors
class ThreadExample {
// 1. Bezpośrednie tworzenie Thread (niezalecane)
fun directThread() {
val thread = Thread(Runnable {
Thread.sleep(1000)
print("Bezpośredni wątek wykonany")
})
thread.start()
}
// 2. HandlerThread do sekwencyjnych zadań w tle
fun handlerThreadExample() {
val handlerThread = HandlerThread("BackgroundQueue")
handlerThread.start()
val handler = Handler(handlerThread.looper)
handler.post {
// Sekwencyjne wykonywanie w wątku tła
Thread.sleep(500)
print("HandlerThread: zadanie wykonane")
}
// Zatrzymanie wątku (wykonywane, gdy zadania się zakończą)
handlerThread.quitSafely()
}
// 3. Executors — pula wątków
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 — nowoczesny standard
suspend fun coroutineExample() = kotlinx.coroutines.withContext(
kotlinx.coroutines.Dispatchers.Default
) {
print("Coroutine on thread: ${Thread.currentThread().getName()}")
}
}
Przykład ThreadExample pokazuje wszystkie cztery poziomy abstrakcji wątków w Android. Bezpośrednie tworzenie Thread to najbardziej niskopoziomowe i nieefektywne podejście. HandlerThread jest przydatny do sekwencyjnych zadań w tle. Executors.newFixedThreadPool(4) tworzy pulę z 4 wątków do równoległego wykonywania do 10 zadań. Kotlin Coroutines z Dispatchers.Default to nowoczesny, efektywny i bezpieczny sposób.
HandlerThread to wyspecjalizowana podklasa Thread z wbudowanym Looper i kolejką komunikatów. Tworzy się go wywołaniem start(), po czym przez Handler(handlerThread.looper) można wysyłać Runnable i komunikaty. HandlerThread wykonuje zadania ściśle sekwencyjnie — następne zadanie nie rozpocznie się przed zakończeniem poprzedniego. Jest to wygodne przy zapisie danych do Room lub plików, gdzie kolejność operacji jest krytyczna. Wywołanie quitSafely() zatrzymuje Looper po zakończeniu bieżącego zadania.
iOS również oferuje trzy poziomy pracy z wątkami. Thread (Thread w Swift, NSThread w Objective-C) — niskopoziomowe API, bezpośrednio tworzące natywny wątek. GCD (Grand Central Dispatch) przez DispatchQueue — główne narzędzie dla programistów iOS, automatycznie zarządzające pulą wątków. OperationQueue — wysokopoziomowa abstrakcja nad GCD z obsługą zależności, priorytetów i anulowania.
Bezpośrednie użycie Thread w nowoczesnym tworzeniu aplikacji iOS zdarza się niezwykle rzadko — GCD zapewnia wszystkie niezbędne możliwości z automatycznym zarządzaniem pamięcią i wątkami. Thread jest używany tylko w konkretnych przypadkach: ustawienie thread-local storage (threadDictionary), utworzenie RunLoop dla wątku tła lub integracja z bibliotekami C oczekującymi pthread_t.
import Foundation
class ThreadManager {
// 1. Thread (niskopoziomowy)
func createThread() {
let thread = Thread {
// Kod wykonuje się w nowym wątku
print("Current thread: \(Thread.current)")
}
thread.name = "com.app.worker"
thread.qualityOfService = .utility
thread.start()
}
// 2. GCD — DispatchQueue
func gcdExample() {
// Kolejka równoległa
let queue = DispatchQueue(label: "com.app.concurrent",
qos: .utility,
attributes: .concurrent)
queue.async {
print("Zadanie asynchroniczne GCD")
}
// Barrier do synchronizacji zapisu
queue.async(flags: .barrier) {
// Ekskluzywny dostęp podczas zapisu
print("Barrier write: dostęp ekskluzywny")
}
}
// 3. OperationQueue z zależnościami
func operationQueueExample() {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2
queue.qualityOfService = .background
let download = BlockOperation {
print("Pobieranie...")
}
let process = BlockOperation {
print("Przetwarzanie...")
}
let save = BlockOperation {
print("Zapisywanie...")
}
// Zależności: download -> process -> save
process.addDependency(download)
save.addDependency(process)
queue.addOperations([download, process, save], waitUntilFinished: false)
}
}
// Kolekcja bezpieczna dla wątków przez 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)
}
}
}
Klasa ThreadSafeArray demonstruje wzorzec Concurrent Read / Exclusive Write przez GCD barrier. Odczyt przez queue.sync{} wykonuje się równolegle z wielu wątków. Zapis przez queue.async(flags: .barrier) blokuje wszystkie inne operacje (zarówno odczyt, jak i zapis) do zakończenia zapisu. Jest to bardziej efektywne niż blokady synchronized, ponieważ nie blokuje czytelników, dopóki nie ma zapisu.
Bezpośrednie użycie Thread w iOS jest uzasadnione w trzech przypadkach: dla thread-local storage (Thread.current.threadDictionary) — przechowywania danych związanych z wątkiem; dla utworzenia specjalnego RunLoop w wątku tła przez performSelector:onThread:; dla integracji z bibliotekami C/C++ oczekującymi pthread_t. We wszystkich innych przypadkach preferowane jest GCD przez DispatchQueue — automatycznie zarządza pulą wątków i zużyciem energii.
Race condition (stan wyścigu) powstaje, gdy dwa lub więcej wątków jednocześnie uzyskuje dostęp do wspólnych danych i co najmniej jeden z nich wykonuje zapis. Wynik zależy od kolejności wykonania (timing) i jest nieprzewidywalny. Aby zapobiec race condition, stosuje się prymitywy synchronizacji. W tworzeniu aplikacji mobilnych dostępne są blokady (synchronized, NSLock), operacje atomowe (AtomicInteger, właściwości atomic w iOS) i kolejki (serial queue).
Wybór prymitywu zależy od scenariusza. Dla prostych liczników i flag wystarczą operacje atomowe (AtomicInteger, atomic property). Dla sekcji krytycznych z wieloma operacjami — blokady (synchronized, NSLock). Dla złożonych struktur danych — serial DispatchQueue lub GCD barrier. Blokady są prostsze do zrozumienia, ale podatne na deadlock i livelock. Kolejki są trudniejsze, ale bezpieczniejsze.
// Synchronizacja w Android/Kotlin
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
class Counter {
// 1. AtomicInteger — dla prostych liczników
private val atomicCount = AtomicInteger(0)
fun incrementAtomic() = atomicCount.incrementAndGet()
// 2. synchronized — dla sekcji krytycznych
@Synchronized
fun synchronizedOperation() {
// Tylko jeden wątek na raz
doWork()
}
// 3. Mutex z korutyn — suspend-safe
private val mutex = Mutex()
suspend fun mutexOperation() {
mutex.withLock {
// protected code — bezpieczne dla wątków
doWork()
}
}
private fun doWork() { /* critical section */ }
}
// Przykład deadlock: A blokuje B, B blokuje 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 demonstruje trzy podejścia do synchronizacji. AtomicInteger.incrementAndGet() — operacja atomowa bez blokad (CAS). @Synchronized — wbudowany monitor Java, blokuje cały obiekt. Mutex.withLock — korutynowy mutex, wstrzymuje korutynę zamiast blokować wątek (bardziej efektywne). DeadlockExample pokazuje klasyczny deadlock: dwa wątki przechwytują blokady w różnej kolejności.
Thread Pool (pula wątków) to zestaw wcześniej utworzonych wątków, które są ponownie wykorzystywane do wykonywania zadań. Zamiast tworzyć nowy wątek dla każdego zadania (drogo), pula pobiera wolny wątek z puli. Jeśli nie ma wolnych wątków, zadanie trafia do kolejki. Pula automatycznie zarządza rozmiarem: nowe wątki są tworzone przy szczytowym obciążeniu, bezczynne wątki są kończone. Zmniejsza to narzut na tworzenie wątków dziesięciokrotnie.
W Android Executors.newFixedThreadPool(4) tworzy pulę z 4 wątków. Jeśli jednocześnie przychodzi 10 zadań, 4 zaczną się natychmiast, a 6 będzie czekać w kolejce. Executors.newCachedThreadPool() tworzy wątki w miarę potrzeb (bez limitu) i kończy bezczynne po 60 sekundach. Dla iOS GCD automatycznie zapewnia pule globalnych kolejek, których rozmiar odpowiada liczbie rdzeni CPU i bieżącemu obciążeniu.
W Kotlin Coroutines pule wątków są ukryte wewnątrz dyspozytorów. Dispatchers.Default używa puli o rozmiarze równym liczbie rdzeni CPU (minimum 2). Dispatchers.IO — 64 wątki (wystarczające dla setek zadań IO-bound, ponieważ większość będzie oczekiwać na wejście-wyjście, nie zajmując CPU). Każdy dyspozytor automatycznie skaluje pulę pod obciążenie, oszczędzając energię baterii w czasie bezczynności.
Często zadawane pytania
Thread to podstawowa jednostka wykonania kodu w aplikacji. Każdy proces może mieć wiele wątków, które dzielą pamięć, ale mają własny stos. W tworzeniu aplikacji mobilnych wątki są używane do równoległego wykonywania zadań bez blokowania UI. Android używa Thread, Executors, HandlerThread i Coroutines. iOS używa Thread, GCD (DispatchQueue) i OperationQueue.
Tworzenie Thread wymaga przydzielenia ~1 MB stosu w Android i ~512 KB w iOS — to kosztowna operacja. Dla 1000 zadań bezpośrednie utworzenie 1000 wątków będzie wymagać ~1 GB tylko na stosy plus koszty ogólne context switch. Zamiast Thread używaj pul (Executors, GCD) lub korutyn — ponownie wykorzystują wątki, zmniejszając narzut dziesięciokrotnie.
Race condition to nieprzewidywalne zachowanie przy jednoczesnym dostępie wielu wątków do wspólnych danych z zapisem. Można jej uniknąć na trzy sposoby: używać typów atomowych (AtomicInteger), blokad (synchronized, NSLock) lub serializować dostęp przez kolejkę (DispatchQueue serial, Actor w Kotlin). Najlepsza praktyka to minimalizowanie wspólnego stanu mutable i używanie immutability.
Thread to natywny obiekt systemowy, zajmujący ~1 MB stosu i powiązany z jądrem systemu operacyjnego. Korutyna to lekka jednostka wykonania Kotlin, która nie jest powiązana z konkretnym wątkiem i może być wstrzymywana (suspend) bez blokowania. Jeden wątek może wykonywać tysiące korutyn. Korutyny są bardziej wydajne pod względem pamięci i pozwalają pisać asynchroniczny kod bez callbacks.
Deadlock objawia się całkowitym zawieszeniem aplikacji bez ANR. W Android użyj Thread.getAllStackTraces() do zrzutu stosów wszystkich wątków — dwa wątki będą czekać na swoje blokady. W iOS — Thread.callStackSymbols. Narzędzia: Android Studio Profiler (zakładka Threads), Instruments (iOS, Thread State View). Zapobieganie: przechwytuj blokady w stałej kolejności, używaj tryLock z limitem czasu.
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.
Przeczytaj również