Thread dans le développement mobile — qu'est-ce que c'est, types et gestion des threads

Auteur : IT Sectr Publié le : 2026-03-16 Temps de lecture : 11 min

Thread — unité de base du temps processeur, possédant sa propre pile et s'exécutant indépendamment des autres threads. Dans le développement mobile, les threads sont utilisés pour l'exécution parallèle des tâches, afin que l'interface reste réactive pendant les opérations longues. Android prend en charge java.lang.Thread, Executors et Kotlin Coroutines, iOS — Thread (Objective-C), GCD et OperationQueue. Selon la Documentation Android Thread, la création d'un thread natif nécessite l'allocation d'environ 1 Mo de pile par le système d'exploitation.

À retenir

  • Thread — unité minimale d'ordonnancement du CPU : chaque thread est indépendant et possède sa propre pile
  • Création d'un thread nécessite ~1 Mo de pile sous Android et 512 Ko sous iOS, donc les pools sont plus efficaces que la création directe
  • Android : Thread, Executors, HandlerThread, Coroutines — quatre niveaux d'abstraction de threads
  • iOS : Thread (bas niveau), GCD (DispatchQueue), OperationQueue (haut niveau)
  • Thread safety — l'accès partagé aux données mutables nécessite une synchronisation : locks, atomic, serial queues

Qu'est-ce qu'un Thread

Thread (fil d'exécution) — est une séquence d'instructions indépendante que le système d'exploitation peut planifier sur un cœur de CPU. Chaque processus (application) contient au moins un thread — le Main Thread. Des threads supplémentaires sont créés pour l'exécution parallèle des tâches. Chaque thread possède sa propre pile logicielle (avec des variables locales), un compteur ordinal (PC) et des registres. La mémoire tas (heap) est partagée par tous les threads du processus.

Dans les systèmes d'exploitation mobiles, les threads sont planifiés par multitâche préemptif (preemptive multitasking) : l'OS peut interrompre l'exécution d'un thread à tout moment et passer la main à un autre (context switch). Le changement de contexte est une opération coûteuse (1-10 microsecondes), car elle nécessite la sauvegarde et la restauration des registres du CPU, la mise à jour du TLB et la vidange des caches. C'est pourquoi un nombre excessif de threads (centaines ou milliers) dégrade les performances — l'OS consacre plus de temps aux changements de contexte qu'à l'exécution.

Thread et processus — sont des concepts différents. Un processus est une instance d'application avec une mémoire virtuelle dédiée. Un thread à l'intérieur d'un processus partage cette mémoire avec d'autres threads. Sous Android, chaque composant d'application (Activity, Service, BroadcastReceiver) fonctionne dans un seul processus, mais peut s'exécuter sur différents threads. Une application iOS est également un seul processus avec la possibilité de créer des threads supplémentaires via GCD ou Thread.

Cycle de vie d'un thread : états et transitions

Chaque thread en Java/Kotlin (Android) et NSThread (iOS) passe par cinq états : New (créé), Runnable (prêt à s'exécuter), Running (s'exécute sur le CPU), Blocked/Waiting (attend une ressource ou une notification), Terminated (terminé). Les transitions entre les états sont gérées par l'ordonnanceur de l'OS et les primitives de synchronisation. Le développeur peut influencer la priorité du thread (Thread.setPriority()) et son état (sleep, join, interrupt).

Sous Android, un thread passe à l'état Blocked lors d'une tentative d'acquisition d'un moniteur occupé (synchronized), d'un appel à Object.wait() ou Thread.sleep(). Sous iOS — lors d'un appel à NSCondition.wait(), pthread_cond_wait() ou dispatch_semaphore_wait(). Dans l'état Blocked, le thread ne consomme pas de CPU, mais occupe de la mémoire (pile). Un thread peut être interrompu (interrupted) depuis un autre thread, recevant InterruptedException (Java) ou vérifiant isCancelled (Kotlin Coroutines).

ÉtatDescriptionMéthode de transition
NewThread créé, mais pas démarréConstructeur Thread()
RunnableThread prêt à s'exécuter, attend le CPUthread.start()
RunningThread s'exécute sur un cœur CPUOrdonnanceur de l'OS
Blocked/WaitingThread attend une ressource, un moniteur ou une notificationsynchronized, wait(), sleep()
TerminatedThread a terminé run() ou a été interrompurun() terminé, interrupt()

Context Switch et son coût

Context switch (changement de contexte) — opération par laquelle l'OS sauvegarde l'état du thread actuel (registres, PC, TLB) et charge l'état sauvegardé d'un autre. Dans les systèmes mobiles (Linux + ART, XNU pour iOS), un context switch prend 1 à 10 microsecondes. Si un thread exécute une tâche en 100 microsecondes et que le context switch en prend 5, alors 5 % du temps est perdu. Pour minimiser les context switches, iOS utilise GCD avec vol de travail (work stealing), Android — des pools avec fixedThreadCount.

Thread sous Android : de Thread à Coroutines

Android a évolué du java.lang.Thread bas niveau aux coroutines modernes. Chaque niveau d'abstraction offre plus de possibilités avec moins de frais généraux. Thread est la classe de base, mais sa création directe n'est pas recommandée : un nouveau thread n'est pas géré par un pool, il est difficile à surveiller et à annuler. AsyncTask (déprécié depuis l'API 30) était un pas en avant, mais souffrait de fuites mémoire et d'une gestion maladroite des configurations.

HandlerThread est une sous-classe spéciale de Thread avec Looper, qui peut traiter une file d'attente de messages. Il est utilisé pour l'exécution séquentielle de tâches sur un thread d'arrière-plan, par exemple pour écrire des données dans Room ou des fichiers. HandlerThread est créé en appelant start(), après quoi via Handler(handlerThread.looper) on peut envoyer des messages et des Runnable. L'appel handlerThread.quit() arrête le Looper et termine le thread.

kotlin
// Android : Thread, HandlerThread et Executors
import android.os.Handler
import android.os.HandlerThread
import java.util.concurrent.Executors

class ThreadExample {

    // 1. Création directe de Thread (non recommandée)
    fun directThread() {
        val thread = Thread(Runnable {
            Thread.sleep(1000)
            print("Direct thread executed")
        })
        thread.start()
    }

    // 2. HandlerThread pour les tâches de fond séquentielles
    fun handlerThreadExample() {
        val handlerThread = HandlerThread("BackgroundQueue")
        handlerThread.start()

        val handler = Handler(handlerThread.looper)
        handler.post {
            // Exécution séquentielle sur un thread d'arrière-plan
            Thread.sleep(500)
            print("HandlerThread : tâche terminée")
        }

        // Arrêt du thread (s'exécute lorsque les tâches sont terminées)
        handlerThread.quitSafely()
    }

    // 3. Executors — pool de threads
    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 — le standard moderne
    suspend fun coroutineExample() = kotlinx.coroutines.withContext(
        kotlinx.coroutines.Dispatchers.Default
    ) {
        print("Coroutine on thread: ${Thread.currentThread().getName()}")
    }
}

L'exemple ThreadExample montre les quatre niveaux d'abstraction des threads sous Android. La création directe de Thread est l'approche la plus bas niveau et la moins efficace. HandlerThread est utile pour les tâches séquentielles en arrière-plan. Executors.newFixedThreadPool(4) crée un pool de 4 threads pour l'exécution parallèle de jusqu'à 10 tâches. Kotlin Coroutines avec Dispatchers.Default est une méthode moderne, efficace et sûre.

HandlerThread : tâches séquentielles en arrière-plan

HandlerThread est une sous-classe spécialisée de Thread avec un Looper intégré et une file d'attente de messages. Il est créé en appelant start(), après quoi via Handler(handlerThread.looper) on peut envoyer des Runnable et des messages. HandlerThread exécute les tâches strictement séquentiellement — la tâche suivante ne commence pas avant la fin de la précédente. C'est pratique pour écrire des données dans Room ou des fichiers, où l'ordre des opérations est critique. L'appel quitSafely() arrête le Looper après la fin de la tâche en cours.

Thread sous iOS : Thread, GCD et OperationQueue

iOS propose également trois niveaux de travail avec les threads. Thread (Thread en Swift, NSThread en Objective-C) — une API bas niveau qui crée directement un thread natif. GCD (Grand Central Dispatch) via DispatchQueue — l'outil principal pour les développeurs iOS, qui gère automatiquement le pool de threads. OperationQueue — une abstraction haut niveau au-dessus de GCD avec prise en charge des dépendances, des priorités et de l'annulation.

L'utilisation directe de Thread dans le développement iOS moderne est extrêmement rare — GCD fournit toutes les fonctionnalités nécessaires avec une gestion automatique de la mémoire et des threads. Thread n'est utilisé que pour des cas spécifiques : configuration d'un stockage thread-local (threadDictionary), création d'un RunLoop pour un thread d'arrière-plan ou intégration avec des bibliothèques C qui attendent pthread_t.

swift
import Foundation

class ThreadManager {

    // 1. Thread (bas niveau)
    func createThread() {
        let thread = Thread {
            // Le code s'exécute sur un nouveau thread
            print("Current thread: \(Thread.current)")
        }
        thread.name = "com.app.worker"
        thread.qualityOfService = .utility
        thread.start()
    }

    // 2. GCD — DispatchQueue
    func gcdExample() {
        // File d'attente parallèle
        let queue = DispatchQueue(label: "com.app.concurrent",
                                 qos: .utility,
                                 attributes: .concurrent)

        queue.async {
            print("GCD async task")
        }

        // Barrier pour la synchronisation d'écriture
        queue.async(flags: .barrier) {
            // Accès exclusif pendant l'écriture
            print("Barrier write: exclusive access")
        }
    }

    // 3. OperationQueue avec dépendances
    func operationQueueExample() {
        let queue = OperationQueue()
        queue.maxConcurrentOperationCount = 2
        queue.qualityOfService = .background

        let download = BlockOperation {
            print("Downloading...")
        }
        let process = BlockOperation {
            print("Processing...")
        }
        let save = BlockOperation {
            print("Saving...")
        }

        // Dépendances : download -> process -> save
        process.addDependency(download)
        save.addDependency(process)

        queue.addOperations([download, process, save], waitUntilFinished: false)
    }
}

// Collection thread-safe via 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)
        }
    }
}

La classe ThreadSafeArray illustre le motif Lecture Concurrente / Écriture Exclusive via GCD barrier. La lecture via queue.sync{} s'exécute en parallèle depuis plusieurs threads. L'écriture via queue.async(flags: .barrier) bloque toutes les autres opérations (lecture et écriture) jusqu'à la fin de l'écriture. C'est plus efficace que les blocs synchronized car cela ne bloque pas les lecteurs lorsqu'il n'y a pas d'écriture.

iOS Thread vs GCD : quand utiliser Thread directement

L'utilisation directe de Thread dans iOS est justifiée dans trois cas : pour le stockage thread-local (Thread.current.threadDictionary) — conservation de données liées au thread ; pour la création d'un RunLoop spécial sur un thread d'arrière-plan avec performSelector:onThread: ; pour l'intégration avec des bibliothèques C/C++ qui attendent pthread_t. Dans tous les autres cas, GCD via DispatchQueue est préférable — il gère automatiquement le pool de threads et la consommation d'énergie.

Synchronisation des threads : locks, atomic, serial queues

Race condition (condition de course) se produit lorsque deux threads ou plus accèdent simultanément à des données partagées, et qu'au moins l'un des threads effectue une écriture. Le résultat dépend de l'ordre d'exécution (timing) et est imprévisible. Pour prévenir les race conditions, on utilise des primitives de synchronisation. Dans le développement mobile, on dispose de verrous (synchronized, NSLock), d'opérations atomiques (AtomicInteger, propriétés atomic d'iOS) et de files d'attente (serial queue).

Le choix de la primitive dépend du scénario. Pour les compteurs simples et les drapeaux, les opérations atomiques suffisent (AtomicInteger, propriété atomic). Pour les sections critiques avec plusieurs opérations — des verrous (synchronized, NSLock). Pour les structures de données complexes — une serial DispatchQueue ou GCD barrier. Les verrous sont plus simples à comprendre, mais sujets aux deadlocks et livelocks. Les files d'attente sont plus complexes mais plus sûres.

kotlin
// Synchronisation dans Android/Kotlin
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

class Counter {

    // 1. AtomicInteger — pour les compteurs simples
    private val atomicCount = AtomicInteger(0)
    fun incrementAtomic() = atomicCount.incrementAndGet()

    // 2. synchronized — pour les sections critiques
    @Synchronized
    fun synchronizedOperation() {
        // Un seul thread à la fois
        doWork()
    }

    // 3. Mutex des coroutines — suspend-safe
    private val mutex = Mutex()
    suspend fun mutexOperation() {
        mutex.withLock {
            // protected code — thread-safe
            doWork()
        }
    }

    private fun doWork() { /* critical section */ }
}

// Exemple de deadlock : A verrouille B, B verrouille 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 illustre trois approches de synchronisation. AtomicInteger.incrementAndGet() — opération atomique sans verrou (CAS). @Synchronized — moniteur intégré Java, verrouille tout l'objet. Mutex.withLock — mutex de coroutine, suspend la coroutine au lieu de bloquer le thread (plus efficace). DeadlockExample montre un deadlock classique : deux threads acquièrent des verrous dans un ordre différent.

Thread Pools : pourquoi Executors est meilleur que Thread

Thread Pool (pool de threads) — un ensemble de threads pré-créés qui sont réutilisés pour exécuter des tâches. Au lieu de créer un nouveau thread pour chaque tâche (coûteux), le pool prend un thread libre du pool. S'il n'y a pas de thread libre, la tâche est mise en file d'attente. Le pool gère automatiquement la taille : de nouveaux threads sont créés lors des pics de charge, les threads inactifs sont terminés. Cela réduit les frais généraux de création de threads de plusieurs dizaines de fois.

Sous Android, Executors.newFixedThreadPool(4) crée un pool de 4 threads. Si 10 tâches arrivent simultanément, 4 commencent immédiatement, 6 attendent dans la file. Executors.newCachedThreadPool() crée des threads selon les besoins (sans limite) et termine les inactifs après 60 secondes. Pour iOS, GCD fournit automatiquement des pools de files d'attente globales, dont la taille correspond au nombre de cœurs CPU et à la charge actuelle.

Dans Kotlin Coroutines, les pools de threads sont cachés à l'intérieur des dispatchers. Dispatchers.Default utilise un pool de taille égale au nombre de cœurs CPU (minimum 2). Dispatchers.IO — 64 threads (suffisant pour des centaines de tâches IO-bound, car la plupart attendront les entrées-sorties sans utiliser le CPU). Chaque dispatcher adapte automatiquement la taille du pool à la charge, économisant l'énergie de la batterie au repos.

Questions fréquentes

Qu'est-ce qu'un Thread dans le développement mobile ?

Thread — unité de base d'exécution de code dans une application. Chaque processus peut avoir plusieurs threads partageant la mémoire, mais avec leur propre pile. Dans le développement mobile, les threads sont utilisés pour l'exécution parallèle de tâches sans bloquer l'UI. Android utilise Thread, Executors, HandlerThread et Coroutines. iOS utilise Thread, GCD (DispatchQueue) et OperationQueue.

Pourquoi n'est-il pas recommandé de créer Thread directement ?

La création de Thread nécessite l'allocation d'environ 1 Mo de pile sous Android et ~512 Ko sous iOS — c'est une opération coûteuse. Pour 1000 tâches, la création directe de 1000 threads nécessiterait environ 1 Go rien que pour les piles, plus les frais généraux de context switch. Au lieu de Thread, utilisez des pools (Executors, GCD) ou des coroutines — ils réutilisent les threads, réduisant les frais généraux de plusieurs dizaines de fois.

Qu'est-ce qu'une race condition et comment l'éviter ?

Race condition — comportement imprévisible lors de l'accès simultané de plusieurs threads à des données partagées avec écriture. On peut l'éviter de trois façons : utiliser des types atomiques (AtomicInteger), des verrous (synchronized, NSLock) ou sérialiser l'accès via une file d'attente (DispatchQueue serial, Actor en Kotlin). La meilleure pratique est de minimiser l'état mutable partagé et d'utiliser l'immutabilité.

Quelle est la différence entre Thread et coroutine ?

Thread — un objet système natif, occupant environ 1 Mo de pile et lié à un cœur d'OS. Coroutine — une unité d'exécution légère de Kotlin, qui n'est pas liée à un thread spécifique et peut se suspendre sans bloquer. Un seul thread peut exécuter des milliers de coroutines. Les coroutines sont plus efficaces en mémoire et permettent d'écrire du code asynchrone sans callbacks.

Comment détecter un deadlock dans une application mobile ?

Deadlock se manifeste par un blocage complet de l'application sans ANR. Sous Android, utilisez Thread.getAllStackTraces() pour obtenir les traces de tous les threads — deux threads attendront mutuellement leurs verrous. Sous iOS — Thread.callStackSymbols. Outils : Android Studio Profiler (onglet Threads), Instruments (iOS, Thread State View). Prévention : acquérez les verrous dans un ordre fixe, utilisez tryLock avec timeout.

Résumé

  • Thread — unité minimale du CPU : exécution indépendante avec sa propre pile, mémoire tas partagée
  • Cinq états du thread : New, Runnable, Running, Blocked/Waiting, Terminated
  • Android a évolué de Thread → AsyncTask → Executors → HandlerThread → Coroutines
  • iOS propose Thread, GCD (DispatchQueue) et OperationQueue — du bas au haut niveau
  • Race condition se résout avec des verrous (synchronized, NSLock), des types atomiques et des serial queues
  • Deadlock survient lors d'une acquisition croisée de verrous — se prévient par un ordre fixe
  • Thread Pool est plus efficace que la création de nouveaux Thread : réutilise les threads, réduit les frais généraux de context switch

Nous développerons une application mobile clé en main

IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.

Discuter du projet

Lisez aussi