Background Thread — нит извршења која није повезана са корисничким интерфејсом, намењена за дуготрајне операције: мрежни захтеви, рад са датотекама, JSON парсинг, компресија слика, шифровање и упити бази података. У iOS-у позадинске нити се управљају кроз GCD (DispatchQueue.global) и OperationQueue, у Android-у — кроз Executors, WorkManager и Kotlin Coroutines (Dispatchers.IO, Dispatchers.Default). Према Apple DispatchQueue Documentation, након завршетка позадинске операције резултат се мора вратити на Main Thread ради ажурирања интерфејса.
Главно
Background Thread — било која нит у апликацији која није Main Thread и нема приступ UI-ју. Његов задатак је да ослободи главну нит од тешких операција како би интерфејс остао одзиван. Оперативни систем распоређује позадинске нити по језгрима процесора, омогућавајући паралелно извршавање више задатака. iOS аутоматски управља pool-ом нити кроз GCD, Android — кроз pool-ове Java Executors.
За разлику од Main Thread-а, који обрађује догађаје секвенцијално (један за другим), позадинске нити се могу извршавати паралелно, ограничене само бројем CPU језгара. На пример, на уређају са 8 језгара може се покренути до 8 паралелних позадинских задатака без значајног успоравања. Међутим, прекомеран број нити (стотине) доводи до thread starvation-а — конкуренције за језгра и раста трошкова на пребацивање контекста (context switch).
Quality of Service (QoS) — механизам iOS-а који омогућава постављање приоритета позадинског задатка. Вредности: .userInteractive (највиши, скоро Main Thread), .userInitiated (корисник очекује резултат), .default (стандардни), .utility (корисник не чека директно), .background (најнижи, за синхронизацију и индексирање). У Android-у аналог је Thread.setPriority() од 1 до 10, али Android такође користи cgroups за групно управљање приоритетима нити.
DispatchQueue.global(qos:) — основни начин добијања позадинског реда у iOS-у. GCD (Grand Central Dispatch) аутоматски креира pool нити и распоређује задатке по језгрима. Позив DispatchQueue.global(qos: .background).async {} шаље блок извршења у позадински ред са најнижим приоритетом. За задатке чији је резултат потребан одмах, користите .userInitiated или .utility.
OperationQueue — апстракција вишег нивоа изнад GCD-а, која омогућава постављање зависности између операција, максималног броја истовремено извршаваних операција (maxConcurrentOperationCount) и приоритета. OperationQueue је згодна за сложене мултитаскинг ланце: преузми датотеку -> отпакуј -> сачувај у кеш. Подразумевано, OperationQueue користи позадинске нити, ако није другачије назначено.
import UIKit
class ImageDownloader {
func downloadImagesSequentially() {
let urls = ["https://example.com/1.png", "https://example.com/2.png"]
// OperationQueue са 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("Загружено: \(url.lastPathComponent)")
}
}
}
}
// GCD: глобални позадински ред са различитим QoS
func backgroundTaskWithQoS() {
DispatchQueue.global(qos: .userInitiated).async {
// Висок приоритет — корисник чека резултат
let result = self.heavyComputation()
DispatchQueue.main.async {
self.showResult(result)
}
}
}
private func heavyComputation() -> String {
Thread.sleep(forTimeInterval: 2) // симулација рада
return "Резултат израчунавања"
}
private func showResult(_ result: String) {
print("Result on Main: \(result)")
}
}
У примеру OperationQueue учитава две слике паралелно (maxConcurrentOperationCount = 2) са позадином кроз qualityOfService = .utility. GCD метод backgroundTaskWithQoS користи глобални ред са .userInitiated за задатак чији резултат корисник очекује. Оба приступа се завршавају повратком на DispatchQueue.main ради ажурирања UI-ја — ово је обавезан захтев iOS-а.
GCD подржава два типа редова: serial (секвенцијални) и concurrent (паралелни). Serial редови извршавају задатке један за другим — ово је згодно за приступ заједничком ресурсу (датотека, база података) без блокирања. Concurrent редови извршавају задатке паралелно, распоређујући их на слободна језгра. DispatchQueue.global — увек concurrent. Да бисте креирали serial ред, користите DispatchQueue(label: "com.app.queue").
Android пружа неколико нивоа апстракције за позадинске нити. Класични приступ — java.util.concurrent.Executors.newFixedThreadPool(n) или Executors.newCachedThreadPool(). Савремени — Kotlin Coroutines са Dispatchers.IO (за улаз-излаз: мрежа, датотеке, база података) и Dispatchers.Default (за CPU-интензивне задатке: сортирање, обрада слика). WorkManager — за одложене и гарантоване позадинске задатке.
HandlerThread — специјална Android класа за креирање позадинске нити са сопственим Looper-ом (редом порука). За разлику од Executors-а, HandlerThread омогућава слање порука и Runnable-а кроз Handler. Користи се за операције које захтевају стављање у ред (нпр. секвенцијални упис у базу података). Након употребе треба позвати quit() или quitSafely() ради ослобађања ресурса.
// Android: Executors и Coroutines Dispatchers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.concurrent.Executors
class DataRepository {
private val ioExecutor = Executors.newFixedThreadPool(4)
// Класични приступ кроз Executors
fun loadDataLegacy(callback: (String) -> Unit) {
ioExecutor.execute {
val result = readFromFile()
val handler = android.os.Handler(android.os.Looper.getMainLooper())
handler.post { callback(result) }
}
}
// Савремени приступ кроз Coroutines
suspend fun loadDataCoroutines(): String {
return withContext(Dispatchers.IO) {
// Операција са датотеком — извршава се у позадинском pool-у
readFromFile()
}
// Резултат се аутоматски враћа на Dispatchers.Main
}
// CPU-интензиван задатак на Dispatchers.Default
suspend fun processImage(pixels: IntArray): IntArray {
return withContext(Dispatchers.Default) {
// Сортирање, филтрирање — извршава се на Default pool-у
pixels.sortedArray()
}
}
private fun readFromFile(): String {
Thread.sleep(1000) // симулација читања из датотеке
return "file_content"
}
fun cleanup() {
ioExecutor.shutdown()
}
}
Пример DataRepository показује еволуцију позадинских нити у Android-у. Legacy метод loadDataLegacy користи Executors.newFixedThreadPool(4) са Handler-ом за повратак на Main Thread. Савремени loadDataCoroutines користи withContext(Dispatchers.IO) — корутина се суспендује током рада, не блокирајући нит, и аутоматски се наставља на Main Thread-у. Dispatchers.Default се препоручује за CPU-bound операције (сортирање, филтрирање, трансформација података).
Kotlin Coroutines — није само начин рада са нитима, већ суштински другачији модел: асинхрони задаци нису везани за одређену нит и могу се суспендовати (suspend) без блокирања. То значи да током рада у позадини, корутина не заузима нит, већ је ослобађа за друге задатке. Механизам suspension омогућава извршавање стотина хиљада concurrent задатака на pool-у од 4-8 нити без thread starvation-а.
Три главна dispatcher-а: Dispatchers.Main (UI, једна нит), Dispatchers.IO (64 нити подразумевано за блокирајуће операције: мрежа, датотеке, база података), Dispatchers.Default (једнако броју CPU језгара, за интензивна израчунавања). Комбинујући их кроз withContext, програмер пребацује између нити без креирања callback-а. withContext је suspend функција која не враћа контролу док задатак не буде завршен.
// Корутине: композиција позадинских задатака
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
suspend fun loadUserProfile(userId: String): UserProfile =
coroutineScope {
// Паралелно учитавање података из различитих извора
val user = async(Dispatchers.IO) { fetchUser(userId) }
val posts = async(Dispatchers.IO) { fetchPosts(userId) }
val avatar = async(Dispatchers.Default) {
processAvatar(fetchAvatar(userId))
}
// await() — суспендује се до завршетка свих задатака
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 }
Функција loadUserProfile покреће три паралелна позадинска задатка кроз async. fetchUser и fetchPosts — IO-bound (мрежа), извршавају се на Dispatchers.IO. processAvatar — CPU-bound (обрада слике), извршава се на Dispatchers.Default. await() суспендује корутину до завршетка свих задатака. Укупно време извршења је једнако максималном времену међу три задатка (500 ms за fetchPosts), а не њиховом збиру. Ово је кључна предност coroutines-а у односу на секвенцијално извршење.
Structured concurrency — принцип по коме свака корутина има родитељски scope, а отказивање родитеља аутоматски отказује дете корутине. У Android-у lifecycleScope отказује све корутине при уништењу Activity-ја. viewModelScope — при чишћењу ViewModel-а. Ово спречава цурење позадинских задатака: ако је корисник затворио екран, позадинска корутина неће наставити да учитава податке који више никоме нису потребни.
WorkManager — Android Jetpack библиотека за извршавање позадинских задатака који морају бити извршени чак и након поновног покретања уређаја или затварања апликације. За разлику од Executors-а и корутина, које живе у процесу апликације, WorkManager предаје задатак системском dispatcher-у који гарантује извршење под одговарајућим условима (доступност мреже, ниво батерије, слободан простор). WorkManager је погодан за синхронизацију података, отпремање логова, прављење резервних копија.
Задатак у WorkManager-у је класа која наслеђује Worker (или CoroutineWorker за корутине). Worker.doWork() се извршава на позадинској нити коју обезбеђује WorkManager. Резултат се враћа кроз Result.success(), Result.retry() или Result.failure(). Задаци се могу комбиновати у ланце: oneTimeWorkRequest.andThen(nextRequest).enqueue(). WorkManager сам бира оптимално време извршења узимајући у обзир ограничења (Constraints).
// WorkManager са корутинама
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 {
// Извршава се на Dispatchers.Default (подразумевано)
return withContext(Dispatchers.IO) {
try {
syncDataToServer()
Result.success()
} catch (e: Exception) {
if (runAttemptCount < 3) Result.retry() else Result.failure()
}
}
}
private suspend fun syncDataToServer() {
// Симулација синхронизације
delay(1000)
}
}
// Покретање WorkManager задатка са ограничењима
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 наслеђује CoroutineWorker — верзију Worker-а која подржава корутине. doWork() се извршава на Dispatchers.Default, пребацивање на IO за мрежне операције кроз withContext. Constraints гарантују да ће синхронизација почети само када постоји мрежа и ниво батерије није нижи од ниског. BackoffCriteria са EXPONENTIAL повећава интервал између поновних покушаја: 10, 20, 40 секунди.
За редовне задатке (синхронизација сваких 15 минута, слање аналитике сваког сата) WorkManager пружа PeriodicWorkRequestBuilder. Минимални интервал — 15 минута. За разлику од OneTimeWorkRequest-а, PeriodicWorkRequest не гарантује тачно поштовање интервала — систем може да комбинује више периодичних задатака ради уштеде батерије. За прецизне интервале користите AlarmManager, али узмите у обзир ограничења Android 12+ за прецизне аларме.
Прва грешка — креирање нове Thread за сваки задатак. new Thread().start() креира нативну нит са алокацијом од ~1 MB за стек. За 100 паралелних задатака то је 100 MB само за стекове, плус трошкови на context switch. Користите pool-ове нити: Executors.newFixedThreadPool(n) (Android) или DispatchQueue.global() (iOS) — они поново користе нити, смањујући overhead десетинама пута.
Друга грешка — приступ mutable стању из више позадинских нити без синхронизације. Ако две позадинске нити истовремено пишу у исти ArrayList или HashMap, настају race condition: ConcurrentModificationException у Android-у, data corruption у iOS-у. Решење: користите thread-safe колекције (ConcurrentHashMap, CopyOnWriteArrayList) или серијализујте приступ кроз један ред (DispatchQueue serial).
Трећа грешка — позадински задаци без управљања животним циклусом. Покретање корутине у глобалном scope-у без везивања за животни циклус Activity-ја или ViewModel-а доводи до цурења: задатак наставља да се извршава након уништења екрана. У Android-у користите lifecycleScope (Activity/Fragment) или viewModelScope (ViewModel). У iOS-у — weak self у closure-има и отказивање задатака при deinit-у.
Често постављана питања
Background Thread — нит на којој се извршавају операције које нису повезане са UI-јем: мрежни захтеви, читање/писање датотека, JSON парсинг, израчунавања. Он ослобађа Main Thread од тешког рада, чувајући одзивност интерфејса. У iOS-у позадинске нити се управљају кроз GCD (DispatchQueue.global), у Android-у — кроз Executors или Kotlin Coroutines (Dispatchers.IO, Dispatchers.Default).
Dispatchers.IO је намењен за блокирајуће операције улаза-излаза: читање датотека, мрежни захтеви, рад са базом података. Има pool од 64 нити. Dispatchers.Default — за CPU-интензивне задатке: сортирање, филтрирање, обрада слика. Његов pool је једнак броју CPU језгара. Коришћење Dispatchers.Default за IO операције може блокирати сва језгра, а Dispatchers.IO за CPU задатке — створити прекомеран број нити.
DispatchQueue.global(qos: .background).async { } шаље блок извршења на глобални позадински ред. Након завршетка позадинског рада потребно је вратити се на главну нит кроз DispatchQueue.main.async { } ради ажурирања UI-ја. За секвенцијалне позадинске задатке користите OperationQueue са maxConcurrentOperationCount = 1 или DispatchQueue(label: "serial").
Препоручени број позадинских нити је једнак броју CPU језгара плус 1 за IO-bound задатке. На савременом 8-језгарном уређају то је 9 нити. Креирање стотина нити доводи до thread starvation-а: оперативни систем троши више времена на пребацивање контекста (context switch) него на извршавање задатака. GCD у iOS-у и Executors у Android-у аутоматски оптимизују pool нити за тренутни уређај.
У Kotlin Coroutines-у повратак на Main Thread се дешава аутоматски ако је корутина покренута у Main-scope-у (lifecycleScope.launch, viewModelScope.launch). Функција withContext(Dispatchers.IO) суспендује корутину на IO нити, а након завршетка је аутоматски наставља на dispatcher-у на ком је покренута (обично Main). Експлицитно позивање DispatchQueue.main.async није потребно.
Резиме
Развићемо мобилну апликацију под кључ
IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође