Thread — η βασική μονάδα χρόνου επεξεργαστή, που έχει τη δική της στοίβα και εκτελείται ανεξάρτητα από άλλα νήματα. Στην ανάπτυξη εφαρμογών για κινητά, τα νήματα χρησιμοποιούνται για την παράλληλη εκτέλεση εργασιών, ώστε το περιβάλλον να παραμένει αποκριτικό κατά τη διάρκεια μακροχρόνιων λειτουργιών. Το Android υποστηρίζει java.lang.Thread, Executors και Kotlin Coroutines, το iOS — Thread (Objective-C), GCD και OperationQueue. Σύμφωνα με το Android Thread Documentation, η δημιουργία ενός εγγενούς νήματος απαιτεί την εκχώρηση ~1 MB για τη στοίβα από το λειτουργικό σύστημα.
Βασικά σημεία
Thread (νήμα εκτέλεσης) — μια ανεξάρτητη ακολουθία εντολών που το λειτουργικό σύστημα μπορεί να προγραμματίσει σε έναν πυρήνα CPU. Κάθε διεργασία (εφαρμογή) περιέχει τουλάχιστον ένα νήμα — Main Thread. Πρόσθετα νήματα δημιουργούνται για την παράλληλη εκτέλεση εργασιών. Κάθε νήμα έχει τη δική του στοίβα προγράμματος (με τοπικές μεταβλητές), μετρητή εντολών (PC) και καταχωρητές. Η μνήμη heap είναι κοινή για όλα τα νήματα της διεργασίας.
Στα λειτουργικά συστήματα κινητών, τα νήματα προγραμματίζονται με προληπτική πολυεπεξεργασία (preemptive multitasking): το λειτουργικό σύστημα μπορεί να διακόψει την εκτέλεση ενός νήματος ανά πάσα στιγμή και να μεταβιβάσει τον έλεγχο σε άλλο (context switch). Η εναλλαγή περιβάλλοντος είναι δαπανηρή λειτουργία (1–10 μικροδευτερόλεπτα), επειδή απαιτεί αποθήκευση/επαναφορά των καταχωρητών CPU, ενημέρωση του TLB και εκκαθάριση των κρυφών μνημών. Γι' αυτό ακριβώς ο υπερβολικός αριθμός νημάτων (εκατοντάδες και χιλιάδες) υποβαθμίζει την απόδοση — το λειτουργικό σύστημα ξοδεύει περισσότερο χρόνο στην εναλλαγή παρά στην εκτέλεση.
Νήμα και διεργασία είναι διαφορετικές έννοιες. Η διεργασία είναι μια παρουσία της εφαρμογής με αποκλειστική εικονική μνήμη. Το νήμα μέσα στη διεργασία μοιράζεται αυτή τη μνήμη με άλλα νήματα. Στο Android, κάθε στοιχείο της εφαρμογής (Activity, Service, BroadcastReceiver) λειτουργεί σε μία διεργασία, αλλά μπορεί να εκτελείται σε διαφορετικά νήματα. Η εφαρμογή iOS είναι επίσης μία διεργασία με δυνατότητα δημιουργίας πρόσθετων νημάτων μέσω GCD ή Thread.
Κάθε νήμα σε Java/Kotlin (Android) και NSThread (iOS) περνά από πέντε καταστάσεις: New (δημιουργήθηκε), Runnable (έτοιμο για εκτέλεση), Running (εκτελείται σε CPU), Blocked/Waiting (περιμένει πόρο ή ειδοποίηση), Terminated (ολοκληρώθηκε). Οι μεταβάσεις μεταξύ των καταστάσεων ελέγχονται από τον χρονοπρογραμματιστή του λειτουργικού συστήματος και τα αρχέγονα συγχρονισμού. Ο προγραμματιστής μπορεί να επηρεάσει την προτεραιότητα του νήματος (Thread.setPriority()) και την κατάστασή του (sleep, join, interrupt).
Στο Android, το νήμα περνά στην κατάσταση Blocked όταν προσπαθεί να καταλάβει έναν κατειλημμένο monitor (synchronized), όταν καλεί Object.wait() ή Thread.sleep(). Στο iOS — όταν καλεί NSCondition.wait(), pthread_cond_wait() ή dispatch_semaphore_wait(). Στην κατάσταση Blocked το νήμα δεν καταναλώνει CPU, αλλά καταλαμβάνει μνήμη (στοίβα). Το νήμα μπορεί να διακοπεί (interrupted) από άλλο νήμα, λαμβάνοντας InterruptedException (Java) ή ελέγχοντας το isCancelled (Kotlin Coroutines).
| Κατάσταση | Περιγραφή | Μέθοδος μετάβασης |
|---|---|---|
| New | Το νήμα δημιουργήθηκε, αλλά δεν ξεκίνησε | Κατασκευαστής Thread() |
| Runnable | Το νήμα είναι έτοιμο για εκτέλεση, περιμένει CPU | thread.start() |
| Running | Το νήμα εκτελείται σε πυρήνα CPU | Χρονοπρογραμματιστής λειτουργικού συστήματος |
| Blocked/Waiting | Το νήμα περιμένει πόρο, monitor ή ειδοποίηση | synchronized, wait(), sleep() |
| Terminated | Το νήμα ολοκλήρωσε το run() ή διακόπηκε | run() ολοκληρώθηκε, interrupt() |
Context switch (εναλλαγή περιβάλλοντος) — η λειτουργία κατά την οποία το λειτουργικό σύστημα αποθηκεύει την κατάσταση του τρέχοντος νήματος (καταχωρητές, PC, TLB) και φορτώνει την αποθηκευμένη κατάσταση ενός άλλου. Στα συστήματα κινητών (Linux + ART, XNU για iOS), το context switch διαρκεί 1–10 μικροδευτερόλεπτα. Αν ένα νήμα εκτελεί μια εργασία σε 100 μικροδευτερόλεπτα και το context switch διαρκεί 5, τότε χάνεται το 5% του χρόνου. Για την ελαχιστοποίηση του context switch, το iOS χρησιμοποιεί GCD με work stealing, και το Android — pools με fixedThreadCount.
Το Android εξελίχθηκε από το χαμηλού επιπέδου java.lang.Thread στα σύγχρονα coroutines. Κάθε επίπεδο αφαίρεσης δίνει περισσότερες δυνατότητες με μικρότερο πρόσθετο κόστος. Το Thread είναι η βασική κλάση, αλλά η άμεση δημιουργία του δεν συνιστάται: το νέο νήμα δεν διαχειρίζεται από pool, είναι δύσκολο να παρακολουθηθεί και να ακυρωθεί. Το AsyncTask (deprecated από το API 30) ήταν ένα βήμα μπροστά, αλλά υπέφερε από διαρροές μνήμης και μη βολική διαχείριση διαμορφώσεων.
HandlerThread — ειδική υποκλάση του Thread με Looper, που μπορεί να επεξεργάζεται ουρά μηνυμάτων. Χρησιμοποιείται για τη σειριακή εκτέλεση εργασιών σε νήμα παρασκηνίου, για παράδειγμα, εγγραφή δεδομένων σε Room ή αρχεία. Το HandlerThread δημιουργείται με την κλήση start(), μετά την οποία μέσω του Handler(handlerThread.looper) μπορούν να αποσταλούν μηνύματα και Runnable. Η κλήση handlerThread.quit() σταματά το Looper και τερματίζει το νήμα.
// Android: Thread, HandlerThread και Executors
import android.os.Handler
import android.os.HandlerThread
import java.util.concurrent.Executors
class ThreadExample {
// 1. Άμεση δημιουργία Thread (δεν συνιστάται)
fun directThread() {
val thread = Thread(Runnable {
Thread.sleep(1000)
print("Άμεσο νήμα εκτελέστηκε")
})
thread.start()
}
// 2. HandlerThread για σειριακές εργασίες παρασκηνίου
fun handlerThreadExample() {
val handlerThread = HandlerThread("BackgroundQueue")
handlerThread.start()
val handler = Handler(handlerThread.looper)
handler.post {
// Σειριακή εκτέλεση σε νήμα παρασκηνίου
Thread.sleep(500)
print("HandlerThread: η εργασία ολοκληρώθηκε")
}
// Διακοπή νήματος (εκτελείται όταν ολοκληρωθούν οι εργασίες)
handlerThread.quitSafely()
}
// 3. Executors — pool νημάτων
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 — σύγχρονο πρότυπο
suspend fun coroutineExample() = kotlinx.coroutines.withContext(
kotlinx.coroutines.Dispatchers.Default
) {
print("Coroutine on thread: ${Thread.currentThread().getName()}")
}
}
Το παράδειγμα ThreadExample δείχνει και τα τέσσερα επίπεδα αφαίρεσης νημάτων στο Android. Η άμεση δημιουργία Thread είναι η χαμηλότερου επιπέδου και λιγότερο αποδοτική προσέγγιση. Το HandlerThread είναι χρήσιμο για σειριακές εργασίες στο παρασκήνιο. Το Executors.newFixedThreadPool(4) δημιουργεί pool 4 νημάτων για παράλληλη εκτέλεση έως 10 εργασιών. Το Kotlin Coroutines με Dispatchers.Default — σύγχρονος, αποδοτικός και ασφαλής τρόπος.
HandlerThread — εξειδικευμένη υποκλάση του Thread με ενσωματωμένο Looper και ουρά μηνυμάτων. Δημιουργείται με την κλήση start(), μετά την οποία μέσω του Handler(handlerThread.looper) μπορούν να αποσταλούν Runnable και μηνύματα. Το HandlerThread εκτελεί τις εργασίες αυστηρά σειριακά — η επόμενη εργασία δεν ξεκινά μέχρι να ολοκληρωθεί η προηγούμενη. Αυτό είναι βολικό για την εγγραφή δεδομένων σε Room ή αρχεία, όπου η σειρά των λειτουργιών είναι κρίσιμη. Η κλήση quitSafely() σταματά το Looper μετά την ολοκλήρωση της τρέχουσας εργασίας.
Το iOS παρέχει επίσης τρία επίπεδα εργασίας με τα νήματα. Thread (Thread σε Swift, NSThread σε Objective-C) — API χαμηλού επιπέδου που δημιουργεί άμεσα εγγενές νήμα. GCD (Grand Central Dispatch) μέσω DispatchQueue — το κύριο εργαλείο για προγραμματιστές iOS, που διαχειρίζεται αυτόματα το pool νημάτων. OperationQueue — αφαίρεση υψηλού επιπέδου πάνω από το GCD με υποστήριξη εξαρτήσεων, προτεραιοτήτων και ακύρωσης.
Η άμεση χρήση του Thread στη σύγχρονη ανάπτυξη iOS είναι εξαιρετικά σπάνια — το GCD παρέχει όλες τις απαραίτητες δυνατότητες με αυτόματη διαχείριση μνήμης και νημάτων. Το Thread χρησιμοποιείται μόνο για συγκεκριμένες περιπτώσεις: ρύθμιση thread-local storage (threadDictionary), δημιουργία RunLoop για νήμα παρασκηνίου ή ενσωμάτωση με βιβλιοθήκες C που περιμένουν pthread_t.
import Foundation
class ThreadManager {
// 1. Thread (χαμηλού επιπέδου)
func createThread() {
let thread = Thread {
// Ο κώδικας εκτελείται σε νέο νήμα
print("Current thread: \(Thread.current)")
}
thread.name = "com.app.worker"
thread.qualityOfService = .utility
thread.start()
}
// 2. GCD — DispatchQueue
func gcdExample() {
// Παράλληλη ουρά
let queue = DispatchQueue(label: "com.app.concurrent",
qos: .utility,
attributes: .concurrent)
queue.async {
print("Ασύγχρονη εργασία GCD")
}
// Barrier για συγχρονισμό εγγραφής
queue.async(flags: .barrier) {
// Αποκλειστική πρόσβαση κατά την εγγραφή
print("Barrier write: αποκλειστική πρόσβαση")
}
}
// 3. OperationQueue με εξαρτήσεις
func operationQueueExample() {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2
queue.qualityOfService = .background
let download = BlockOperation {
print("Λήψη...")
}
let process = BlockOperation {
print("Επεξεργασία...")
}
let save = BlockOperation {
print("Αποθήκευση...")
}
// Εξαρτήσεις: download -> process -> save
process.addDependency(download)
save.addDependency(process)
queue.addOperations([download, process, save], waitUntilFinished: false)
}
}
// Thread-safe συλλογή μέσω 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)
}
}
}
Η κλάση ThreadSafeArray επιδεικνύει το πρότυπο Concurrent Read / Exclusive Write μέσω GCD barrier. Η ανάγνωση μέσω queue.sync{} εκτελείται παράλληλα από πολλά νήματα. Η εγγραφή μέσω queue.async(flags: .barrier) μπλοκάρει όλες τις άλλες λειτουργίες (τόσο ανάγνωση όσο και εγγραφή) μέχρι να ολοκληρωθεί η εγγραφή. Αυτό είναι πιο αποδοτικό από τα μπλοκ synchronized, επειδή δεν μπλοκάρει τους αναγνώστες όσο δεν υπάρχει εγγραφή.
Η άμεση χρήση του Thread στο iOS δικαιολογείται σε τρεις περιπτώσεις: για thread-local storage (Thread.current.threadDictionary) — αποθήκευση δεδομένων που συνδέονται με το νήμα· για τη δημιουργία ειδικού RunLoop σε νήμα παρασκηνίου με performSelector:onThread:· για ενσωμάτωση με βιβλιοθήκες C/C++ που περιμένουν pthread_t. Σε όλες τις άλλες περιπτώσεις, το GCD μέσω DispatchQueue είναι προτιμότερο — διαχειρίζεται αυτόματα το pool νημάτων και την κατανάλωση ενέργειας.
Race condition (συνθήκη ανταγωνισμού) προκύπτει όταν δύο ή περισσότερα νήματα αποκτούν ταυτόχρονα πρόσβαση σε κοινά δεδομένα και τουλάχιστον ένα από αυτά εκτελεί εγγραφή. Το αποτέλεσμα εξαρτάται από τη σειρά εκτέλεσης (timing) και είναι απρόβλεπτο. Για την πρόληψη της race condition χρησιμοποιούνται αρχέγονα συγχρονισμού. Στην ανάπτυξη εφαρμογών για κινητά διατίθενται κλειδώματα (synchronized, NSLock), ατομικές λειτουργίες (AtomicInteger, ιδιότητες atomic στο iOS) και ουρές (serial queue).
Η επιλογή του αρχέγονου εξαρτάται από το σενάριο. Για απλούς μετρητές και σημαίες αρκούν οι ατομικές λειτουργίες (AtomicInteger, atomic property). Για κρίσιμα τμήματα με πολλές λειτουργίες — τα κλειδώματα (synchronized, NSLock). Για πολύπλοκες δομές δεδομένων — serial DispatchQueue ή GCD barrier. Τα κλειδώματα είναι πιο εύκολα στην κατανόηση, αλλά είναι επιρρεπή σε deadlock και livelock. Οι ουρές είναι πιο πολύπλοκες, αλλά πιο ασφαλείς.
// Συγχρονισμός σε Android/Kotlin
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
class Counter {
// 1. AtomicInteger — για απλούς μετρητές
private val atomicCount = AtomicInteger(0)
fun incrementAtomic() = atomicCount.incrementAndGet()
// 2. synchronized — για κρίσιμα τμήματα
@Synchronized
fun synchronizedOperation() {
// Μόνο ένα νήμα τη φορά
doWork()
}
// 3. Mutex από coroutines — suspend-safe
private val mutex = Mutex()
suspend fun mutexOperation() {
mutex.withLock {
// protected code — thread-safe
doWork()
}
}
private fun doWork() { /* critical section */ }
}
// Παράδειγμα deadlock: A μπλοκάρει το B, B μπλοκάρει το 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 επιδεικνύει τρεις προσεγγίσεις συγχρονισμού. AtomicInteger.incrementAndGet() — ατομική λειτουργία χωρίς κλειδώματα (CAS). @Synchronized — ο ενσωματωμένος monitor της Java, μπλοκάρει ολόκληρο το αντικείμενο. Mutex.withLock — coroutine mutex, αναστέλλει το coroutine αντί να μπλοκάρει το νήμα (πιο αποδοτικό). Το DeadlockExample δείχνει ένα κλασικό deadlock: δύο νήματα αποκτούν τα κλειδώματα με διαφορετική σειρά.
Thread Pool (pool νημάτων) — ένα σύνολο προδημιουργημένων νημάτων που επαναχρησιμοποιούνται για την εκτέλεση εργασιών. Αντί να δημιουργείται νέο νήμα για κάθε εργασία (δαπανηρό), το pool παίρνει ένα ελεύθερο νήμα από το pool. Αν δεν υπάρχουν ελεύθερα νήματα, η εργασία μπαίνει σε ουρά. Το pool διαχειρίζεται αυτόματα το μέγεθος: νέα νήματα δημιουργούνται σε αιχμές φόρτου, τα αδρανή νήματα τερματίζονται. Αυτό μειώνει το κόστος δημιουργίας νημάτων δεκαπλασίως.
Στο Android, το Executors.newFixedThreadPool(4) δημιουργεί pool 4 νημάτων. Αν έρθουν ταυτόχρονα 10 εργασίες, οι 4 θα ξεκινήσουν αμέσως και οι 6 θα περιμένουν στην ουρά. Το Executors.newCachedThreadPool() δημιουργεί νήματα ανάλογα με τις ανάγκες (χωρίς όριο) και τερματίζει τα αδρανή μετά από 60 δευτερόλεπτα. Για το iOS, το GCD παρέχει αυτόματα pools καθολικών ουρών, το μέγεθος των οποίων αντιστοιχεί στον αριθμό των πυρήνων CPU και στο τρέχον φορτίο.
Στα Kotlin Coroutines, τα pools νημάτων είναι κρυμμένα μέσα στους dispatchers. Το Dispatchers.Default χρησιμοποιεί pool μεγέθους ίσου με τον αριθμό των πυρήνων CPU (τουλάχιστον 2). Το Dispatchers.IO — 64 νήματα (αρκετά για εκατοντάδες εργασίες IO-bound, καθώς οι περισσότερες θα περιμένουν είσοδο-έξοδο χωρίς να καταλαμβάνουν CPU). Κάθε dispatcher κλιμακώνει αυτόματα το pool ανάλογα με το φορτίο, εξοικονομώντας ενέργεια μπαταρίας σε κατάσταση αδράνειας.
Συχνές ερωτήσεις
Thread — η βασική μονάδα εκτέλεσης κώδικα σε μια εφαρμογή. Κάθε διεργασία μπορεί να έχει πολλά νήματα που μοιράζονται τη μνήμη, αλλά με δική τους στοίβα. Στην ανάπτυξη εφαρμογών για κινητά, τα νήματα χρησιμοποιούνται για παράλληλη εκτέλεση εργασιών χωρίς μπλοκάρισμα του UI. Το Android χρησιμοποιεί Thread, Executors, HandlerThread και Coroutines. Το iOS χρησιμοποιεί Thread, GCD (DispatchQueue) και OperationQueue.
Η δημιουργία Thread απαιτεί εκχώρηση ~1 MB για στοίβα σε Android και ~512 KB σε iOS — είναι δαπανηρή λειτουργία. Για 1000 εργασίες, η άμεση δημιουργία 1000 νημάτων θα απαιτήσει ~1 GB μόνο για τις στοίβες συν το πρόσθετο κόστος για context switch. Αντί για Thread, χρησιμοποιήστε pools (Executors, GCD) ή coroutines — επαναχρησιμοποιούν τα νήματα, μειώνοντας το κόστος δεκαπλασίως.
Race condition — απρόβλεπτη συμπεριφορά όταν πολλά νήματα αποκτούν ταυτόχρονα πρόσβαση σε κοινά δεδομένα με εγγραφή. Μπορεί να αποφευχθεί με τρεις τρόπους: χρήση ατομικών τύπων (AtomicInteger), κλειδωμάτων (synchronized, NSLock) ή σειριοποίηση της πρόσβασης μέσω ουράς (serial DispatchQueue, Actor σε Kotlin). Βέλτιστη πρακτική — ελαχιστοποίηση της κοινής mutable κατάστασης και χρήση immutability.
Thread — εγγενές αντικείμενο συστήματος που καταλαμβάνει ~1 MB στοίβας και συνδέεται με τον πυρήνα του λειτουργικού συστήματος. Coroutine — ελαφριά μονάδα εκτέλεσης στο Kotlin που δεν συνδέεται με συγκεκριμένο νήμα και μπορεί να ανασταλεί (suspend) χωρίς μπλοκάρισμα. Ένα νήμα μπορεί να εκτελεί χιλιάδες coroutines. Τα coroutines είναι πιο αποδοτικά στη μνήμη και επιτρέπουν την εγγραφή ασύγχρονου κώδικα χωρίς callbacks.
Deadlock εκδηλώνεται ως πλήρες πάγωμα της εφαρμογής χωρίς ANR. Στο Android χρησιμοποιήστε Thread.getAllStackTraces() για dump των στοιβών όλων των νημάτων — δύο νήματα θα περιμένουν τα κλειδώματα το ένα του άλλου. Στο iOS — Thread.callStackSymbols. Εργαλεία: Android Studio Profiler (καρτέλα Threads), Instruments (iOS, Thread State View). Πρόληψη: αποκτάτε τα κλειδώματα με σταθερή σειρά, χρησιμοποιήστε tryLock με χρονικό όριο.
Συμπεράσματα
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης