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 διαχειρίζεται αυτόματα την ομάδα νημάτων μέσω GCD, το Android — μέσω ομάδων 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) δημιουργεί αυτόματα μια ομάδα νημάτων και κατανέμει τις εργασίες στους πυρήνες. Η κλήση 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) {
// Λειτουργία αρχείου — εκτελείται στην ομάδα παρασκηνίου
readFromFile()
}
// Το αποτέλεσμα επιστρέφεται αυτόματα στο Dispatchers.Main
}
// Εργασία έντονης CPU στο Dispatchers.Default
suspend fun processImage(pixels: IntArray): IntArray {
return withContext(Dispatchers.Default) {
// Ταξινόμηση, φιλτράρισμα — εκτελείται στην ομάδα Default
pixels.sortedArray()
}
}
private fun readFromFile(): String {
Thread.sleep(1000) // προσομοίωση ανάγνωσης από αρχείο
return "file_content"
}
fun cleanup() {
ioExecutor.shutdown()
}
}
Το παράδειγμα DataRepository δείχνει την εξέλιξη των νημάτων παρασκηνίου στο Android. Η παλαιότερη μέθοδος loadDataLegacy χρησιμοποιεί Executors.newFixedThreadPool(4) με Handler για επιστροφή στο Main Thread. Η σύγχρονη loadDataCoroutines χρησιμοποιεί withContext(Dispatchers.IO) — το coroutine αναστέλλεται κατά τη διάρκεια της εργασίας, χωρίς να μπλοκάρει το νήμα, και συνεχίζεται αυτόματα στο Main Thread. Το Dispatchers.Default συνιστάται για λειτουργίες CPU-bound (ταξινόμηση, φιλτράρισμα, μετασχηματισμός δεδομένων).
Kotlin Coroutines — δεν είναι απλώς ένας τρόπος εργασίας με νήματα, αλλά ένα θεμελιωδώς διαφορετικό μοντέλο: οι ασύγχρονες εργασίες δεν είναι δεσμευμένες σε ένα συγκεκριμένο νήμα και μπορούν να ανασταλούν (suspend) χωρίς μπλοκάρισμα. Αυτό σημαίνει ότι κατά την εργασία παρασκηνίου, το coroutine δεν καταλαμβάνει ένα νήμα, αλλά το απελευθερώνει για άλλες εργασίες. Ο μηχανισμός suspension επιτρέπει την εκτέλεση εκατοντάδων χιλιάδων ταυτόχρονων εργασιών σε μια ομάδα 4-8 νημάτων χωρίς thread starvation.
Τρεις κύριοι dispatchers: Dispatchers.Main (UI, ένα νήμα), Dispatchers.IO (από προεπιλογή 64 νήματα για λειτουργίες μπλοκαρίσματος: δίκτυο, αρχεία, βάση δεδομένων), Dispatchers.Default (ίσος με τον αριθμό των πυρήνων CPU, για εντατικούς υπολογισμούς). Συνδυάζοντάς τους μέσω withContext, ο προγραμματιστής αλλάζει μεταξύ νημάτων χωρίς να δημιουργεί callback. Το withContext είναι μια συνάρτηση suspend που δεν επιστρέφει τον έλεγχο μέχρι να ολοκληρωθεί η εργασία.
// Coroutines: σύνθεση εργασιών παρασκηνίου
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() αναστέλλει το coroutine μέχρι την ολοκλήρωση όλων των εργασιών. Ο συνολικός χρόνος εκτέλεσης είναι ίσος με τον μέγιστο χρόνο μεταξύ των τριών εργασιών (500 ms για fetchPosts), όχι το άθροισμά τους. Αυτό είναι το βασικό πλεονέκτημα των coroutines έναντι της σειριακής εκτέλεσης.
Structured concurrency — η αρχή σύμφωνα με την οποία κάθε coroutine έχει ένα γονικό scope, και η ακύρωση του γονέα ακυρώνει αυτόματα τα θυγατρικά coroutines. Στο Android, το lifecycleScope ακυρώνει όλα τα coroutines κατά την καταστροφή του Activity. viewModelScope — κατά τον καθαρισμό του ViewModel. Αυτό αποτρέπει τις διαρροές εργασιών παρασκηνίου: εάν ο χρήστης έκλεισε την οθόνη, το coroutine παρασκηνίου δεν θα συνεχίσει να φορτώνει δεδομένα που δεν χρειάζεται πλέον κανείς.
WorkManager — βιβλιοθήκη Android Jetpack για την εκτέλεση εργασιών παρασκηνίου που πρέπει να εκτελούνται ακόμη και μετά από επανεκκίνηση της συσκευής ή κλείσιμο της εφαρμογής. Σε αντίθεση με τα Executors και τα coroutines, που ζουν στη διεργασία της εφαρμογής, το WorkManager παραδίδει την εργασία σε έναν dispatcher συστήματος που εγγυάται την εκτέλεση υπό κατάλληλες συνθήκες (διαθεσιμότητα δικτύου, επίπεδο μπαταρίας, ελεύθερος χώρος). Το WorkManager είναι κατάλληλο για συγχρονισμό δεδομένων, μεταφόρτωση αρχείων καταγραφής, δημιουργία αντιγράφων ασφαλείας.
Μια εργασία στο WorkManager είναι μια κλάση που κληρονομεί το Worker (ή CoroutineWorker για coroutines). Το Worker.doWork() εκτελείται σε ένα νήμα παρασκηνίου που παρέχεται από το WorkManager. Το αποτέλεσμα επιστρέφεται μέσω Result.success(), Result.retry() ή Result.failure(). Οι εργασίες μπορούν να συνδυαστούν σε αλυσίδες: oneTimeWorkRequest.andThen(nextRequest).enqueue(). Το WorkManager επιλέγει μόνο του τον βέλτιστο χρόνο εκτέλεσης λαμβάνοντας υπόψη τους περιορισμούς (Constraints).
// WorkManager με coroutines
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 που υποστηρίζει coroutines. Το 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). Χρησιμοποιήστε ομάδες νημάτων: Executors.newFixedThreadPool(n) (Android) ή DispatchQueue.global() (iOS) — επαναχρησιμοποιούν νήματα, μειώνοντας το overhead δεκάδες φορές.
Δεύτερο λάθος — πρόσβαση σε μεταβλητή κατάσταση από πολλαπλά νήματα παρασκηνίου χωρίς συγχρονισμό. Εάν δύο νήματα παρασκηνίου γράφουν ταυτόχρονα στο ίδιο ArrayList ή HashMap, προκύπτουν race condition: ConcurrentModificationException στο Android, καταστροφή δεδομένων στο iOS. Λύση: χρησιμοποιήστε thread-safe συλλογές (ConcurrentHashMap, CopyOnWriteArrayList) ή σειριοποιήστε την πρόσβαση μέσω μιας ουράς (DispatchQueue serial).
Τρίτο λάθος — εργασίες παρασκηνίου χωρίς διαχείριση κύκλου ζωής. Η εκκίνηση ενός coroutine σε καθολικό scope χωρίς σύνδεση με τον κύκλο ζωής του Activity ή του ViewModel οδηγεί σε διαρροές: η εργασία συνεχίζεται μετά την καταστροφή της οθόνης. Στο Android, χρησιμοποιήστε lifecycleScope (Activity/Fragment) ή viewModelScope (ViewModel). Στο iOS — weak self σε closures και ακύρωση εργασιών κατά το deinit.
Συχνές ερωτήσεις
Background Thread — το νήμα στο οποίο εκτελούνται λειτουργίες που δεν σχετίζονται με το UI: αιτήματα δικτύου, ανάγνωση/εγγραφή αρχείων, ανάλυση JSON, υπολογισμοί. Απελευθερώνει το Main Thread από βαριά εργασία, διατηρώντας την αποκρισιμότητα της διεπαφής. Στο iOS, τα νήματα παρασκηνίου διαχειρίζονται μέσω GCD (DispatchQueue.global), στο Android — μέσω Executors ή Kotlin Coroutines (Dispatchers.IO, Dispatchers.Default).
Dispatchers.IO προορίζεται για λειτουργίες μπλοκαρίσματος εισόδου-εξόδου: ανάγνωση αρχείων, αιτήματα δικτύου, εργασία με βάση δεδομένων. Έχει ομάδα 64 νημάτων. Dispatchers.Default — για εργασίες έντονης CPU: ταξινόμηση, φιλτράρισμα, επεξεργασία εικόνων. Η ομάδα του είναι ίση με τον αριθμό των πυρήνων 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 βελτιστοποιούν αυτόματα την ομάδα νημάτων για την τρέχουσα συσκευή.
Στο Kotlin Coroutines, η επιστροφή στο Main Thread γίνεται αυτόματα εάν το coroutine εκκινήθηκε σε Main-scope (lifecycleScope.launch, viewModelScope.launch). Η συνάρτηση withContext(Dispatchers.IO) αναστέλλει το coroutine στο νήμα IO, και μετά την ολοκλήρωση το συνεχίζει αυτόματα στον dispatcher όπου εκκινήθηκε (συνήθως Main). Δεν απαιτείται ρητή κλήση του DispatchQueue.main.async.
Περίληψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης