Background Thread — wątek wykonawczy niezwiązany z interfejsem użytkownika, przeznaczony do długotrwałych operacji: zapytania sieciowe, praca z plikami, parsowanie JSON, kompresja obrazów, szyfrowanie i zapytania do bazy danych. W iOS wątkami tła zarządza się przez GCD (DispatchQueue.global) i OperationQueue, w Android — przez Executors, WorkManager i Kotlin Coroutines (Dispatchers.IO, Dispatchers.Default). Według Apple DispatchQueue Documentation, po zakończeniu operacji w tle wynik należy zwrócić do Main Thread w celu aktualizacji interfejsu.
Najważniejsze
Background Thread — dowolny wątek w aplikacji, który nie jest Main Thread i nie ma dostępu do UI. Jego zadaniem jest odciążenie głównego wątku od ciężkich operacji, aby interfejs pozostał responsywny. System operacyjny rozdziela wątki tła na rdzenie procesora, umożliwiając równoległe wykonywanie wielu zadań. iOS automatycznie zarządza pulą wątków przez GCD, Android — przez pule Java Executors.
W przeciwieństwie do Main Thread, który przetwarza zdarzenia sekwencyjnie (jedno po drugim), wątki tła mogą być wykonywane równolegle, ograniczone tylko liczbą rdzeni CPU. Na przykład na urządzeniu z 8 rdzeniami można uruchomić do 8 równoległych zadań w tle bez znaczącego spowolnienia. Jednak nadmierna liczba wątków (setki) prowadzi do thread starvation — rywalizacji o rdzenie i wzrostu narzutów na przełączanie kontekstu (context switch).
Quality of Service (QoS) — mechanizm iOS, który pozwala określić priorytet zadania w tle. Wartości: .userInteractive (najwyższy, prawie Main Thread), .userInitiated (użytkownik oczekuje wyniku), .default (standardowy), .utility (użytkownik nie czeka bezpośrednio), .background (najniższy, do synchronizacji i indeksowania). W Android odpowiednikiem jest Thread.setPriority() od 1 do 10, ale Android używa również cgroups do grupowego zarządzania priorytetami wątków.
DispatchQueue.global(qos:) — podstawowy sposób uzyskania kolejki tła w iOS. GCD (Grand Central Dispatch) automatycznie tworzy pulę wątków i rozdziela zadania na rdzenie. Wywołanie DispatchQueue.global(qos: .background).async {} wysyła blok wykonania do kolejki tła z najniższym priorytetem. Dla zadań, których wynik jest potrzebny natychmiast, używaj .userInitiated lub .utility.
OperationQueue — bardziej wysokopoziomowa abstrakcja nad GCD, która pozwala określać zależności między operacjami, maksymalną liczbę jednocześnie wykonywanych operacji (maxConcurrentOperationCount) i priorytety. OperationQueue jest wygodna dla złożonych wielozadaniowych łańcuchów: pobierz plik -> rozpakuj -> zapisz w cache. Domyślnie OperationQueue używa wątków tła, jeśli nie określono inaczej.
import UIKit
class ImageDownloader {
func downloadImagesSequentially() {
let urls = ["https://example.com/1.png", "https://example.com/2.png"]
// OperationQueue z maxConcurrentOperationCount = 2
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2
queue.qualityOfService = .utility
for urlString in urls {
queue.addOperation {
guard let url = URL(string: urlString),
let data = try? Data(contentsOf: url)
else { return }
DispatchQueue.main.async {
print("Załadowano: \(url.lastPathComponent)")
}
}
}
}
// GCD: globalna kolejka tła z różnymi QoS
func backgroundTaskWithQoS() {
DispatchQueue.global(qos: .userInitiated).async {
// Wysoki priorytet — użytkownik czeka na wynik
let result = self.heavyComputation()
DispatchQueue.main.async {
self.showResult(result)
}
}
}
private func heavyComputation() -> String {
Thread.sleep(forTimeInterval: 2) // symulacja pracy
return "Wynik obliczeń"
}
private func showResult(_ result: String) {
print("Result on Main: \(result)")
}
}
W przykładzie OperationQueue ładuje dwa obrazy równolegle (maxConcurrentOperationCount = 2) z tłem przez qualityOfService = .utility. Metoda GCD backgroundTaskWithQoS używa globalnej kolejki z .userInitiated dla zadania, którego wynik użytkownik oczekuje. Oba podejścia kończą się powrotem na DispatchQueue.main w celu aktualizacji UI — to obowiązkowy wymóg iOS.
GCD obsługuje dwa typy kolejek: serial (sekwencyjne) i concurrent (równoległe). Kolejki serial wykonują zadania jedno po drugim — jest to wygodne dla dostępu do wspólnego zasobu (plik, BazaDanych) bez blokad. Kolejki concurrent wykonują zadania równolegle, rozdzielając je na wolne rdzenie. DispatchQueue.global — zawsze concurrent. Aby utworzyć kolejkę serial, użyj DispatchQueue(label: "com.app.queue").
Android udostępnia kilka poziomów abstrakcji dla wątków tła. Klasyczne podejście — java.util.concurrent.Executors.newFixedThreadPool(n) lub Executors.newCachedThreadPool(). Nowoczesne — Kotlin Coroutines z Dispatchers.IO (dla wejścia-wyjścia: sieć, pliki, BazaDanych) i Dispatchers.Default (dla zadań intensywnie korzystających z CPU: sortowanie, przetwarzanie obrazów). WorkManager — dla odroczonych i gwarantowanych zadań w tle.
HandlerThread — specjalna klasa Androida do tworzenia wątku tła z własnym Looper (kolejką komunikatów). W przeciwieństwie do Executors, HandlerThread pozwala wysyłać komunikaty i Runnable przez Handler. Używany do operacji wymagających kolejkowania (np. sekwencyjny zapis do bazy danych). Po użyciu HandlerThread należy wywołać quit() lub quitSafely() w celu zwolnienia zasobów.
// Android: Executors i Coroutines Dispatchers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.concurrent.Executors
class DataRepository {
private val ioExecutor = Executors.newFixedThreadPool(4)
// Klasyczne podejście przez Executors
fun loadDataLegacy(callback: (String) -> Unit) {
ioExecutor.execute {
val result = readFromFile()
val handler = android.os.Handler(android.os.Looper.getMainLooper())
handler.post { callback(result) }
}
}
// Nowoczesne podejście przez Coroutines
suspend fun loadDataCoroutines(): String {
return withContext(Dispatchers.IO) {
// Operacja plikowa — wykonywana w puli tła
readFromFile()
}
// Wynik automatycznie wraca na Dispatchers.Main
}
// Zadanie CPU-intensywne na Dispatchers.Default
suspend fun processImage(pixels: IntArray): IntArray {
return withContext(Dispatchers.Default) {
// Sortowanie, filtracja — wykonywane na Default-puli
pixels.sortedArray()
}
}
private fun readFromFile(): String {
Thread.sleep(1000) // symulacja odczytu z pliku
return "file_content"
}
fun cleanup() {
ioExecutor.shutdown()
}
}
Przykład DataRepository pokazuje ewolucję wątków tła w Android. Metoda legacy loadDataLegacy używa Executors.newFixedThreadPool(4) z Handler do powrotu na Main Thread. Nowoczesny loadDataCoroutines używa withContext(Dispatchers.IO) — korutyna wstrzymuje się na czas pracy, nie blokując wątku, i automatycznie wznawia na Main Thread. Dispatchers.Default jest zalecany dla operacji CPU-bound (sortowanie, filtracja, transformacja danych).
Kotlin Coroutines — to nie tylko sposób pracy z wątkami, ale zasadniczo inny model: zadania asynchroniczne nie są przypisane do konkretnego wątku i mogą się wstrzymywać (suspend) bez blokowania. Oznacza to, że podczas pracy w tle korutyna nie zajmuje wątku, ale zwalnia go dla innych zadań. Mechanizm suspension pozwala wykonywać setki tysięcy concurrent zadań na puli 4-8 wątków bez thread starvation.
Trzy główne dyspatchery: Dispatchers.Main (UI, jeden wątek), Dispatchers.IO (64 wątki domyślnie dla operacji blokujących: sieć, pliki, BazaDanych), Dispatchers.Default (równa liczbie rdzeni CPU, dla intensywnych obliczeń). Łącząc je przez withContext, programista przełącza się między wątkami bez tworzenia callbacków. withContext to funkcja suspend, która nie zwraca sterowania, dopóki zadanie nie zostanie wykonane.
// Korutyny: kompozycja zadań w tle
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
suspend fun loadUserProfile(userId: String): UserProfile =
coroutineScope {
// Równoległe ładowanie danych z różnych źródeł
val user = async(Dispatchers.IO) { fetchUser(userId) }
val posts = async(Dispatchers.IO) { fetchPosts(userId) }
val avatar = async(Dispatchers.Default) {
processAvatar(fetchAvatar(userId))
}
// await() — wstrzymuje się do zakończenia wszystkich zadań
UserProfile(
user = user.await(),
posts = posts.await(),
avatar = avatar.await()
)
}
data class UserProfile(
val user: String,
val posts: List<String>,
val avatar: ByteArray
)
suspend fun fetchUser(id: String): String { delay(300); return "User:$id" }
suspend fun fetchPosts(id: String): List<String> { delay(500); return listOf("Post1") }
suspend fun fetchAvatar(id: String): ByteArray { delay(200); return ByteArray(1024) }
suspend fun processAvatar(data: ByteArray): ByteArray { delay(100); return data }
Funkcja loadUserProfile uruchamia trzy równoległe zadania w tle przez async. fetchUser i fetchPosts — IO-bound (sieć), wykonują się na Dispatchers.IO. processAvatar — CPU-bound (przetwarzanie obrazu), wykonuje się na Dispatchers.Default. await() wstrzymuje korutynę do zakończenia wszystkich zadań. Całkowity czas wykonania jest równy maksymalnemu czasowi spośród trzech zadań (500 ms dla fetchPosts), a nie ich sumie. To kluczowa zaleta coroutines w porównaniu z sekwencyjnym wykonaniem.
Structured concurrency — zasada, w której każda korutyna ma parent scope, a anulowanie rodzica automatycznie anuluje korutyny potomne. W Android lifecycleScope anuluje wszystkie korutyny przy zniszczeniu Activity. viewModelScope — przy czyszczeniu ViewModel. Zapobiega to wyciekom zadań w tle: jeśli użytkownik zamknął ekran, korutyna w tle nie będzie kontynuować ładowania danych, które już nikomu nie są potrzebne.
WorkManager — biblioteka Android Jetpack do wykonywania zadań w tle, które muszą zostać wykonane nawet po restarcie urządzenia lub zamknięciu aplikacji. W przeciwieństwie do Executors i korutyn, które żyją w procesie aplikacji, WorkManager przekazuje zadanie dyspozytorowi systemu, który gwarantuje wykonanie przy odpowiednich warunkach (dostępność sieci, poziom naładowania baterii, wolne miejsce). WorkManager nadaje się do synchronizacji danych, wysyłania logów, tworzenia kopii zapasowych.
Zadanie w WorkManager to klasa dziedzicząca Worker (lub CoroutineWorker dla korutyn). Worker.doWork() wykonuje się na wątku tła udostępnionym przez WorkManager. Wynik zwracany jest przez Result.success(), Result.retry() lub Result.failure(). Zadania można łączyć w łańcuchy: oneTimeWorkRequest.andThen(nextRequest).enqueue(). WorkManager sam wybiera optymalny czas wykonania z uwzględnieniem ograniczeń (Constraints).
// WorkManager z korutynami
import android.content.Context
import androidx.work.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class SyncWorker(
appContext: Context,
workerParams: WorkerParameters
) : CoroutineWorker(appContext, workerParams) {
override suspend fun doWork(): Result {
// Wykonuje się na Dispatchers.Default (domyślnie)
return withContext(Dispatchers.IO) {
try {
syncDataToServer()
Result.success()
} catch (e: Exception) {
if (runAttemptCount < 3) Result.retry() else Result.failure()
}
}
}
private suspend fun syncDataToServer() {
// Symulacja synchronizacji
delay(1000)
}
}
// Uruchomienie zadania WorkManager z ograniczeniami
fun scheduleSync(context: Context) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
val syncWork = OneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, java.util.concurrent.TimeUnit.SECONDS)
.build()
WorkManager.getInstance(context).enqueue(syncWork)
}
SyncWorker dziedziczy CoroutineWorker — wersję Worker obsługującą korutyny. doWork() wykonuje się na Dispatchers.Default, przełączenie na IO dla operacji sieciowych przez withContext. Constraints gwarantują, że synchronizacja uruchomi się tylko przy dostępności sieci i poziomie naładowania baterii nie niższym niż niski. BackoffCriteria z EXPONENTIAL zwiększa interwał między kolejnymi próbami: 10, 20, 40 sekund.
Dla regularnych zadań (synchronizacja co 15 minut, wysyłanie analityki co godzinę) WorkManager udostępnia PeriodicWorkRequestBuilder. Minimalny interwał — 15 minut. W przeciwieństwie do OneTimeWorkRequest, PeriodicWorkRequest nie gwarantuje dokładnego przestrzegania interwału — system może łączyć kilka okresowych zadań w celu oszczędzania baterii. Dla dokładnych interwałów użyj AlarmManager, ale uwzględnij ograniczenia Android 12+ dotyczące dokładnych alarmów.
Pierwszy błąd — tworzenie nowego Thread dla każdego zadania. new Thread().start() tworzy natywny wątek z alokacją ~1 MB na stos. Dla 100 równoległych zadań to 100 MB tylko na stosy, plus narzuty na context switch. Używaj pul wątków: Executors.newFixedThreadPool(n) (Android) lub DispatchQueue.global() (iOS) — ponownie wykorzystują wątki, zmniejszając overhead dziesiątki razy.
Drugi błąd — dostęp do mutable stanu z wielu wątków tła bez synchronizacji. Jeśli dwa wątki tła jednocześnie zapisują do tego samego ArrayList lub HashMap, powstają race condition: ConcurrentModificationException w Android, data corruption w iOS. Rozwiązanie: używaj kolekcji bezpiecznych wątkowo (ConcurrentHashMap, CopyOnWriteArrayList) lub serializuj dostęp przez jedną kolejkę (DispatchQueue serial).
Trzeci błąd — zadania w tle bez zarządzania cyklem życia. Uruchomienie korutyny w globalnym scope bez powiązania z cyklem życia Activity lub ViewModel prowadzi do wycieków: zadanie kontynuuje działanie po zniszczeniu ekranu. W Android używaj lifecycleScope (Activity/Fragment) lub viewModelScope (ViewModel). W iOS — weak self w domknięciach i anulowanie zadań przy deinit.
Często zadawane pytania
Background Thread — wątek, na którym wykonywane są operacje niezwiązane z UI: zapytania sieciowe, odczyt/zapis plików, parsowanie JSON, obliczenia. Zwalnia Main Thread z ciężkiej pracy, zachowując responsywność interfejsu. W iOS wątkami tła zarządza się przez GCD (DispatchQueue.global), w Android — przez Executors lub Kotlin Coroutines (Dispatchers.IO, Dispatchers.Default).
Dispatchers.IO przeznaczony jest dla blokujących operacji wejścia-wyjścia: odczyt plików, zapytania sieciowe, praca z bazą danych. Ma pulę 64 wątków. Dispatchers.Default — dla zadań intensywnie korzystających z CPU: sortowanie, filtracja, przetwarzanie obrazów. Jego pula jest równa liczbie rdzeni CPU. Użycie Dispatchers.Default dla operacji IO może zablokować wszystkie rdzenie, a Dispatchers.IO dla zadań CPU — stworzyć nadmierną liczbę wątków.
DispatchQueue.global(qos: .background).async { } wysyła blok wykonania do globalnej kolejki tła. Po zakończeniu pracy w tle należy wrócić do głównego wątku przez DispatchQueue.main.async { } w celu aktualizacji UI. Dla sekwencyjnych zadań w tle używaj OperationQueue z maxConcurrentOperationCount = 1 lub DispatchQueue(label: "serial").
Zalecana liczba wątków tła jest równa liczbie rdzeni CPU plus 1 dla zadań IO-bound. Na nowoczesnym 8-rdzeniowym urządzeniu to 9 wątków. Tworzenie setek wątków prowadzi do thread starvation: system operacyjny spędza więcej czasu na przełączaniu kontekstu (context switch) niż na wykonywaniu zadań. GCD w iOS i Executors w Android automatycznie optymalizują pulę wątków pod bieżące urządzenie.
W Kotlin Coroutines powrót do Main Thread następuje automatycznie, jeśli korutyna została uruchomiona w Main-scope (lifecycleScope.launch, viewModelScope.launch). Funkcja withContext(Dispatchers.IO) wstrzymuje korutynę na IO-wątku, a po zakończeniu automatycznie wznawia ją na dyspatcherze, w którym została uruchomiona (zwykle Main). Wyraźne wywołanie DispatchQueue.main.async nie jest wymagane.
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ż