Thread w tworzeniu aplikacji mobilnych — co to jest, rodzaje i zarządzanie wątkami

Autor: IT Sectr Opublikowano: 2026-03-16 Czas czytania: 11 min

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 — minimalna jednostka planowania CPU: każdy wątek jest niezależny i ma własny stos
  • Tworzenie wątku wymaga ~1 MB stosu w Android i 512 KB w iOS, dlatego pule są bardziej wydajne niż bezpośrednie tworzenie
  • Android: Thread, Executors, HandlerThread, Coroutines — cztery poziomy abstrakcji wątków
  • iOS: Thread (niskopoziomowy), GCD (DispatchQueue), OperationQueue (wysokopoziomowy)
  • Thread safety — wspólny dostęp do danych mutable wymaga synchronizacji: locks, atomic, serial queues

Co to jest Thread

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.

Cykl życia wątku: stany i przejścia

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 i jego koszt

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.

Thread w Android: od Thread do Coroutines

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.

kotlin
// 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: sekwencyjne zadania w tle

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.

Thread w iOS: Thread, GCD i OperationQueue

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.

swift
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.

iOS Thread vs GCD: kiedy używać Thread bezpośrednio

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.

Synchronizacja wątków: locks, atomic, serial queues

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.

kotlin
// 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.

Pule wątków: dlaczego Executors jest lepszy niż Thread

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

Co to jest Thread w tworzeniu aplikacji mobilnych?

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.

Dlaczego nie zaleca się tworzenia Thread bezpośrednio?

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.

Co to jest race condition i jak jej uniknąć?

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.

Czym różni się Thread od korutyny?

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.

Jak wyłapać deadlock w aplikacji mobilnej?

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

  • Thread — minimalna jednostka CPU: niezależne wykonanie z własnym stosem, pamięć sterty wspólna
  • Pięć stanów wątku: New, Runnable, Running, Blocked/Waiting, Terminated
  • Android ewoluował od Thread → AsyncTask → Executors → HandlerThread → Coroutines
  • iOS zapewnia Thread, GCD (DispatchQueue) i OperationQueue — od niskiego do wysokiego poziomu
  • Race condition rozwiązuje się blokadami (synchronized, NSLock), typami atomowymi i kolejami serial
  • Deadlock powstaje przy krzyżowym przechwytywaniu blokad — zapobiega mu stała kolejność
  • Thread Pool jest bardziej efektywny niż tworzenie nowych Thread: ponownie wykorzystuje wątki, zmniejsza narzut na context switch

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.

Omów projekt

Przeczytaj również