Thread Pool — to mechanizm zarządzania wątkami, w którym wcześniej utworzona pula wątków jest ponownie wykorzystywana do wykonywania zadań, unikając narzutów związanych z tworzeniem i niszczeniem wątków. W programowaniu mobilnym pula wątków jest używana do operacji w tle: zapytań sieciowych, przetwarzania obrazów, pracy z bazami danych. Według Google Android Documentation (2025), ExecutorService jest zalecanym sposobem zarządzania wątkami w tle w Android. W iOS analogiczną rolę pełnią OperationQueue i GCD DispatchQueue z globalnymi kolejkami współbieżnymi.
Najważniejsze
Thread Pool (pula wątków) — to wzorzec architektoniczny, w którym stała liczba wątków jest tworzona z wyprzedzeniem i ponownie wykorzystywana do wykonywania wielu zadań. Zamiast tworzyć nowy wątek dla każdej operacji (co jest kosztowne: około 1 MB stosu na wątek w JVM), zadania są umieszczane w kolejce i wykonywane przez wolne wątki z puli. W programowaniu mobilnym pula wątków jest krytyczna dla wydajności — Android i iOS ograniczają liczbę wątków na aplikację.
Tworzenie wątku to kosztowna operacja: alokacja stosu, rejestracja w systemie, przełączanie kontekstu. Na urządzeniach mobilnych z ograniczonymi zasobami niekontrolowane tworzenie wątków prowadzi do OOM (OutOfMemoryError) w Android i do throttlingu w iOS. Thread Pool rozwiązuje oba problemy: ogranicza maksymalną liczbę jednocześnie działających wątków i ponownie wykorzystuje już utworzone. Google zaleca ExecutorService zamiast raw Thread(), Apple zaleca OperationQueue zamiast Thread.
| Parametr | Bez puli (raw Thread) | Z Thread Pool |
|---|---|---|
| Tworzenie wątku | Dla każdego zadania | Raz przy tworzeniu puli |
| Maksimum wątków | Nieograniczone (ryzyko OOM) | Ograniczone przez core/max pool size |
| Wykorzystanie | Niskie (wątek umiera po zadaniu) | Wysokie (wątek jest ponownie wykorzystywany) |
| Zarządzanie | Ręczne (join, interrupt) | Automatyczne (ExecutorService) |
| Zużycie pamięci | Rośnie z każdym zadaniem | Stałe |
Pula wątków działa na zasadzie Producer-Consumer: zadania (Runnable/Callable) są umieszczane w blokującej kolejce (BlockingQueue). Wątki z puli oczekują na zadania w kolejce i pobierają je do wykonania. Algorytm: jeśli wolnych wątków jest mniej niż corePoolSize, tworzony jest nowy wątek. Jeśli corePoolSize został osiągnięty, zadanie jest umieszczane w kolejce. Jeśli kolejka jest pełna i wątków jest mniej niż maximumPoolSize, tworzony jest dodatkowy wątek. Po przekroczeniu maximumPoolSize zadanie jest odrzucane przez RejectedExecutionHandler.
Core pool size — liczba wątków utrzymywanych w puli nawet w stanie bezczynności. Maximum pool size — maksymalna liczba wątków, która może zostać utworzona przy przepełnieniu kolejki. Różnica między nimi to dodatkowe (overflow) wątki, które są tworzone tymczasowo i kończone po przekroczeniu czasu bezczynności. Na urządzeniach mobilnych zaleca się ustawienie corePoolSize równego maximumPoolSize, aby uniknąć skokowych obciążeń związanych z tworzeniem wątków.
BlockingQueue przechowuje zadania oczekujące na wykonanie. Najpopularniejsze implementacje: LinkedBlockingQueue (nieograniczona), ArrayBlockingQueue (ograniczona) i SynchronousQueue (bez przechowywania — zadanie jest natychmiast przekazywane do wątku). Przy przepełnieniu kolejki i puli uruchamiany jest RejectedExecutionHandler. Standardowe polityki: AbortPolicy (rzuca RejectedExecutionException), CallerRunsPolicy (wykonuje w wątku nadawcy), DiscardPolicy i DiscardOldestPolicy.
// Tworzenie Thread Pool w Android
val threadPool = ThreadPoolExecutor(
corePoolSize = 2, // Minimum 2 wątki
maximumPoolSize = 4, // Maksimum 4 wątki
keepAliveTime = 30L, // Czas życia wątku nadmiarowego
unit = TimeUnit.SECONDS,
workQueue = LinkedBlockingQueue<Runnable>(16),
threadFactory = Executors.defaultThreadFactory(),
handler = ThreadPoolExecutor.CallerRunsPolicy()
)
// Wysyłanie zadań
threadPool.execute {
val result = api.fetchData()
runOnUiThread { showData(result) }
}
// Zakończenie puli
threadPool.shutdown()
// Oczekiwanie na zakończenie wszystkich zadań
threadPool.awaitTermination(10, TimeUnit.SECONDS)
Android udostępnia kilka implementacji puli wątków przez java.util.concurrent. Executors — fabryka z gotowymi konfiguracjami: newFixedThreadPool(n) (stała pula), newCachedThreadPool() (nieograniczona, wątki tworzone w razie potrzeby), newSingleThreadExecutor() (jeden wątek — sekwencyjne wykonywanie). W projektach mobilnych zaleca się newFixedThreadPool z rozsądnym limitem (2-4 wątki), ponieważ cached pool może utworzyć zbyt wiele wątków.
ThreadPoolExecutor (TPE) — pełna implementacja ExecutorService z konfigurowalnymi parametrami. W Android TPE jest używany wewnątrz AsyncTask, IntentService i JobIntentService. Parametry corePoolSize, maximumPoolSize, keepAliveTime, BlockingQueue i RejectedExecutionHandler pozwalają precyzyjnie dostroić zachowanie puli. Zalecenia dla Android: corePoolSize = liczba rdzeni CPU - 1 (dla zadań IO-bound) lub liczba rdzeni (dla zadań CPU-bound). Dla typowych aplikacji — 2-4 wątki.
// Gotowe konfiguracje Executors
// 1. Stała pula na 3 wątki
val fixedPool = Executors.newFixedThreadPool(3)
// 2. Buforowana pula (niezalecana dla urządzeń mobilnych)
val cachedPool = Executors.newCachedThreadPool()
// 3. Pojedynczy wątek (serializacja)
val singlePool = Executors.newSingleThreadExecutor()
// 4. Planista (zadania okresowe)
val scheduler = Executors.newScheduledThreadPool(2)
// Użycie z Callable i Future
val future: Future<String> = fixedPool.submit(Callable {
"Result: ${api.call()}"
})
// Pobieranie wyniku (blokuje wątek)
val result = future.get(5, TimeUnit.SECONDS)
// Zakończenie puli
fixedPool.shutdownNow()
Kotlin korutyny udostępniają CoroutineDispatcher — abstrakcję analogiczną do puli wątków. Dispatchers.IO używa puli 64 wątków (ograniczonych). Dispatchers.Default — pula równa liczbie rdzeni CPU. CoroutineDispatcher nie wymaga ręcznego shutdown i jest zarządzany automatycznie. Do precyzyjnego dostrojenia utwórz własny ExecutorCoroutineDispatcher przez Executors.newFixedThreadPool(2).asCoroutineDispatcher(). Korutyny nie zastępują puli wątków, ale ją opakowują.
iOS udostępnia dwa główne mechanizmy zarządzania pulą wątków: OperationQueue (wysokopoziomowe API oparte na GCD) i GCD DispatchQueue (niskopoziomowe C-API). OperationQueue hermetyzuje pulę wątków przez właściwość maxConcurrentOperationCount. Domyślnie OperationQueue używa system-defined maximum (zależnego od obciążenia systemu). DispatchQueue.global() udostępnia kolejkę współbieżną z systemową pulą wątków.
OperationQueue zarządza pulą wątków przez maxConcurrentOperationCount. Wartość 1 tworzy kolejkę szeregową (analogicznie do single thread pool). Wartość większa niż 1 — pula współbieżna z określonym limitem. Domyślnie maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount (optymalna systemowo, zwykle 4-8 wątków). Operation obsługuje zależności, priorytety i anulowanie. Każda operacja jest wykonywana na dowolnym wolnym wątku z systemowej puli.
// OperationQueue z pulą na 3 wątki
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 3
queue.qualityOfService = .utility
// Tworzenie operacji
let operation1 = BlockOperation {
let data = fetchData(from: url1)
DispatchQueue.main.async { updateUI(data) }
}
let operation2 = BlockOperation {
let data = fetchData(from: url2)
DispatchQueue.main.async { updateUI(data) }
}
// Zależność: operation2 czeka na operation1
operation2.addDependency(operation1)
// Dodawanie do kolejki
queue.addOperations([operation1, operation2], waitUntilFinished: false)
// Anulowanie wszystkich operacji
queue.cancelAllOperations()
DispatchQueue — to pula wątków od Apple. Kolejka współbieżna (qos: .utility) używa systemowej puli wątków, zoptymalizowanej pod aktualne obciążenie urządzenia. Różne QoS (userInteractive, userInitiated, utility, background) są mapowane na różne pule z różnymi priorytetami. DispatchGroup pozwala synchronizować wiele zadań. DispatchWorkItem obsługuje anulowanie i qualityOfService. Do precyzyjnej kontroli twórz własne kolejki współbieżne przez DispatchQueue(label: qos: attributes: .concurrent).
// GCD DispatchQueue jako pula wątków
let customQueue = DispatchQueue(
label: "com.app.background",
qos: .utility,
attributes: .concurrent,
autoreleaseFrequency: .workItem
)
// Wysyłanie zadań do puli
customQueue.async { self.processFile(file1) }
customQueue.async { self.processFile(file2) }
// DispatchGroup do synchronizacji
let group = DispatchGroup()
let pool = DispatchQueue.global(qos: .utility)
pool.async(group: group) { fetchData() }
pool.async(group: group) { processImage() }
group.notify(queue: .main) {
self.showResult() // Obie zadania zakończone
}
// Ograniczenie współbieżności przez semaphore
let semaphore = DispatchSemaphore(value: 3)
for url in urls {
pool.async {
semaphore.wait()
download(url)
semaphore.signal()
}
}
Konfiguracja puli wątków bezpośrednio wpływa na wydajność aplikacji. Nieprawidłowe parametry prowadzą do niewykorzystania CPU (zbyt mało wątków) lub przeciążenia systemu (zbyt wiele). W aplikacjach mobilnych optymalne wartości różnią się od serwerowych ze względu na ograniczone zasoby i zużycie energii. Główne parametry: corePoolSize, maxPoolSize, queue capacity i keepAliveTime.
Wzór dla zadań IO-bound: corePoolSize = liczba rdzeni CPU × 2 (wątki oczekują na wejście-wyjście). Dla zadań CPU-bound: corePoolSize = liczba rdzeni CPU (wątki są stale obciążone obliczeniami). Na nowoczesnych urządzeniach mobilnych (6-8 rdzeni) daje to 6-8 wątków dla CPU-bound i 12-16 dla IO-bound. Praktyczne testy pokazują, że dla typowej aplikacji mobilnej 3-4 wątki są optymalne — więcej wątków zwiększa zużycie energii bez przyrostu wydajności.
Rozmiar kolejki zadań (work queue) określa, ile zadań może oczekiwać na wykonanie. Nieograniczona kolejka (LinkedBlockingQueue bez limitu) może prowadzić do OOM przy szybkim napływie zadań. Ograniczona kolejka (ArrayBlockingQueue o stałym rozmiarze) odrzuca zadania przy przepełnieniu. W aplikacjach mobilnych zaleca się ArrayBlockingQueue o pojemności 16-32 zadań. CallerRunsPolicy — najlepszy RejectedExecutionHandler dla urządzeń mobilnych: spowalnia nadawcę (pressure back) zamiast tracić zadanie.
| Parametr | Zalecenie dla mobilnych | Uzasadnienie |
|---|---|---|
| corePoolSize | 2-4 | Ograniczone zasoby urządzenia mobilnego |
| maxPoolSize | corePoolSize (lub +1-2) | Uniknięcie skokowych obciążeń przy tworzeniu wątków |
| keepAliveTime | 15-30 sekund | Szybkie zwalnianie pamięci, ale bez częstego tworzenia |
| Queue capacity | 16-32 | Równowaga między buforowaniem a ryzykiem OOM |
| Handler | CallerRunsPolicy | Backpressure bez utraty zadań |
Programiści aplikacji mobilnych często popełniają błędy przy użyciu puli wątków, które prowadzą do awarii, wycieków pamięci i niestabilnej pracy. Najczęstsze: brak wywołania shutdown() dla ExecutorService, tworzenie nowej puli dla każdej operacji, zbyt duża pula, deadlock między zadaniami, używanie CachedThreadPool w Android.
Deadlock występuje, gdy zadanie w puli oczekuje na wynik innego zadania z tej samej puli, ale wszystkie wątki są zajęte oczekiwaniem. Przykład: zadanie A wysyła zadanie B do tej samej puli i wywołuje future.get() — jeśli pula jest wyczerpana, zadanie A czeka na zadanie B, a zadanie B nie może się wykonać, ponieważ nie ma wolnych wątków. Rozwiązanie: używaj oddzielnych pul dla różnych poziomów zadań lub async callback zamiast blokującego .get().
// Deadlock w Thread Pool
val pool = Executors.newFixedThreadPool(1)
// Zadanie A czeka na zadanie B — deadlock!
val futureA = pool.submit {
// To zadanie nigdy się nie wykona
val futureB = pool.submit { 42 }
futureB.get() // Blokowane na zawsze
}
// Rozwiązanie: oddzielne pule
val workerPool = Executors.newFixedThreadPool(2)
val callbackPool = Executors.newSingleThreadExecutor()
workerPool.submit {
callbackPool.submit {
// Wykonywane w oddzielnej puli — deadlock niemożliwy
}
}
// Lub użyj CompletableFuture
workerPool.submit {
CompletableFuture
.supplyAsync { 42 }
.thenAccept { result ->
println(result)
}
}
ExecutorService utworzony w Activity musi być zakończony w onDestroy(). Jeśli tego nie zrobisz, wątki będą wisieć w pamięci nawet po zniszczeniu Activity. Rozwiązanie: przechowuj pulę w zakresie Application lub ViewModel, a nie w Activity. Dla korutyn używaj viewModelScope lub lifecycleScope. Jeśli pula została utworzona wewnątrz Activity, koniecznie wywołaj pool.shutdown() w onDestroy(). Do testów używaj es.shutdownNow() do natychmiastowego zatrzymania.
Często zadawane pytania
Thread Pool ponownie wykorzystuje już utworzone wątki do wykonywania wielu zadań. Zwykły wątek (raw Thread) jest tworzony, wykonuje jedno zadanie i jest niszczony. Utworzenie wątku zajmuje około 1 MB pamięci i ~1 ms czasu. Thread Pool zmniejsza narzuty, ogranicza maksymalną liczbę wątków i udostępnia API do zarządzania (shutdown, awaitTermination).
Dla typowej aplikacji mobilnej optymalne są 2-4 wątki. Dla zadań CPU-bound — liczba rdzeni CPU. Dla zadań IO-bound — liczba rdzeni × 2. Większa liczba wątków zwiększa zużycie energii i przełączanie kontekstu bez przyrostu wydajności. W Android używaj Process.availableProcessors() do określenia rdzeni. W iOS — ProcessInfo.processInfo.processorCount.
CachedThreadPool tworzy wątki w miarę potrzeb i ponownie wykorzystuje istniejące. Problem: nie ogranicza maksymalnej liczby wątków. Jeśli 100 zadań nadejdzie jednocześnie, zostanie utworzonych 100 wątków. Prowadzi to do OOM w Android (każdy wątek ~1 MB). Używaj newFixedThreadPool(n) z jawnym limitem. CachedThreadPool jest dopuszczalny tylko dla krótkotrwałych zadań burst z gwarancją małego wolumenu.
Tak, jeśli pula nie należy do zarządzanego kontenera (jak korutyny). shutdown() zatrzymuje przyjmowanie nowych zadań i kończy wątki po wykonaniu bieżących. Bez shutdown() wątki wiszą w pamięci, aplikacja nie kończy się. Dla Activity wywołuj w onDestroy(). Dla ViewModel używaj coroutineScope. Zakończenie puli to obowiązkowa część zarządzania zasobami, analogiczna do zamykania Cursor lub InputStream.
Tak, OperationQueue i DispatchQueue to pula wątków udostępniana przez iOS. OperationQueue ogranicza współbieżność przez maxConcurrentOperationCount. DispatchQueue.global() używa systemowej puli wątków bez bezpośredniej kontroli. W przeciwieństwie do Java ThreadPoolExecutor, nie zarządzasz corePoolSize ani queue capacity — system optymalizuje pulę automatycznie pod bieżące obciążenie i zużycie energii urządzenia.
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ż