Thread Pool dans le développement mobile — bases, pool de threads et principe de fonctionnement

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

Thread Pool — est un mécanisme de gestion des threads dans lequel un pool de threads pré-créé est réutilisé pour exécuter des tâches, évitant ainsi les frais généraux de création et de destruction de threads. Dans le développement mobile, le thread pool est utilisé pour les opérations en arrière-plan : requêtes réseau, traitement d'images, travail avec des bases de données. Selon la documentation Google Android (2025), ExecutorService est la méthode recommandée pour gérer les threads en arrière-plan sous Android. Sous iOS, OperationQueue et GCD DispatchQueue avec des files d'attente concurrentes globales jouent un rôle similaire.

Points clés

  • Thread Pool — un pool de threads réutilisables pour exécuter des tâches en arrière-plan sans les frais généraux de création de threads.
  • ExecutorService sous Android gère le pool via ThreadPoolExecutor avec des paramètres configurables.
  • OperationQueue sous iOS encapsule un thread pool via maxConcurrentOperationCount.
  • Core pool size — le nombre minimum de threads toujours prêts à exécuter des tâches.
  • Work queue stocke les tâches en attente d'un thread libre dans le pool.

Qu'est-ce que Thread Pool ?

Thread Pool (pool de threads) — est un modèle architectural dans lequel un nombre fixe de threads est créé à l'avance et réutilisé pour exécuter plusieurs tâches. Au lieu de créer un nouveau thread pour chaque opération (ce qui est coûteux : environ 1 Mo de pile par thread dans la JVM), les tâches sont placées dans une file d'attente et exécutées par les threads disponibles du pool. Dans le développement mobile, le thread pool est critique pour les performances — Android et iOS limitent le nombre de threads par application.

Pourquoi Thread Pool est important dans le développement mobile

Créer un thread est une opération coûteuse : allocation de pile, enregistrement système, changement de contexte. Sur les appareils mobiles aux ressources limitées, la création incontrôlée de threads conduit à OOM (OutOfMemoryError) sous Android et à un étranglement sous iOS. Thread Pool résout les deux problèmes : il limite le nombre maximum de threads s'exécutant simultanément et réutilise les threads déjà créés. Google recommande ExecutorService plutôt que raw Thread(), Apple recommande OperationQueue plutôt que Thread.

ParamètreSans pool (raw Thread)Avec Thread Pool
Création de threadPour chaque tâcheUne fois à la création du pool
Maximum de threadsIllimité (risque OOM)Limité par core/max pool size
UtilisationFaible (le thread meurt après la tâche)Élevée (le thread est réutilisé)
GestionManuelle (join, interrupt)Automatique (ExecutorService)
Consommation mémoireAugmente avec chaque tâcheFixe

Comment fonctionne Thread Pool dans le développement mobile ?

Le thread pool fonctionne sur le principe Producteur-Consommateur : les tâches (Runnable/Callable) sont placées dans une file d'attente bloquante (BlockingQueue). Les threads du pool attendent les tâches dans la file et les récupèrent pour exécution. Algorithme : si le nombre de threads libres est inférieur à corePoolSize, un nouveau thread est créé. Si corePoolSize est atteint, la tâche est placée dans la file. Si la file est pleine et que le nombre de threads est inférieur à maximumPoolSize, un thread supplémentaire est créé. Si maximumPoolSize est dépassé, la tâche est rejetée via RejectedExecutionHandler.

Core Pool Size vs Maximum Pool Size

Core pool size — le nombre de threads qui sont conservés dans le pool même inactifs. Maximum pool size — le nombre maximum de threads pouvant être créés lors du débordement de la file. La différence entre eux correspond aux threads supplémentaires (overflow) créés temporairement et terminés après le délai d'inactivité. Sur les appareils mobiles, il est recommandé de définir corePoolSize égal à maximumPoolSize pour éviter les pics de charge liés à la création de threads.

Work Queue et RejectedExecutionHandler

BlockingQueue stocke les tâches en attente d'exécution. Les implémentations les plus populaires : LinkedBlockingQueue (illimitée), ArrayBlockingQueue (limitée) et SynchronousQueue (sans stockage — la tâche est transmise directement à un thread). Lorsque la file et le pool sont pleins, RejectedExecutionHandler est déclenché. Les politiques standard : AbortPolicy (lève RejectedExecutionException), CallerRunsPolicy (exécute dans le thread de l'appelant), DiscardPolicy et DiscardOldestPolicy.

kotlin
// Création de Thread Pool sous Android
val threadPool = ThreadPoolExecutor(
    corePoolSize = 2,        // Minimum 2 threads
    maximumPoolSize = 4,     // Maximum 4 threads
    keepAliveTime = 30L,     // Durée de vie du thread de débordement
    unit = TimeUnit.SECONDS,
    workQueue = LinkedBlockingQueue<Runnable>(16),
    threadFactory = Executors.defaultThreadFactory(),
    handler = ThreadPoolExecutor.CallerRunsPolicy()
)

// Soumission des tâches
threadPool.execute {
    val result = api.fetchData()
    runOnUiThread { showData(result) }
}

// Arrêt du pool
threadPool.shutdown()
// Attente de la fin de toutes les tâches
threadPool.awaitTermination(10, TimeUnit.SECONDS)

Thread Pool sous Android : ExecutorService

Android fournit plusieurs implémentations de thread pool via java.util.concurrent. Executors — une fabrique avec des configurations prêtes à l'emploi : newFixedThreadPool(n) (pool fixe), newCachedThreadPool() (illimité, threads créés selon les besoins), newSingleThreadExecutor() (thread unique — exécution séquentielle). Pour les projets mobiles, newFixedThreadPool avec une limite raisonnable (2-4 threads) est recommandé, car le pool mis en cache peut créer trop de threads.

ThreadPoolExecutor sous Android

ThreadPoolExecutor (TPE) — une implémentation complète d'ExecutorService avec des paramètres configurables. Sous Android, TPE est utilisé à l'intérieur d'AsyncTask, IntentService et JobIntentService. Les paramètres corePoolSize, maximumPoolSize, keepAliveTime, BlockingQueue et RejectedExecutionHandler permettent d'affiner le comportement du pool. Recommandations pour Android : corePoolSize = nombre de cœurs CPU - 1 (pour les tâches IO-bound) ou nombre de cœurs (pour les tâches CPU-bound). Pour les applications typiques : 2-4 threads.

kotlin
// Configurations prêtes d'Executors
// 1. Pool fixe de 3 threads
val fixedPool = Executors.newFixedThreadPool(3)

// 2. Pool mis en cache (non recommandé pour mobile)
val cachedPool = Executors.newCachedThreadPool()

// 3. Thread unique (sérialisation)
val singlePool = Executors.newSingleThreadExecutor()

// 4. Planificateur (tâches périodiques)
val scheduler = Executors.newScheduledThreadPool(2)

// Utilisation avec Callable et Future
val future: Future<String> = fixedPool.submit(Callable {
    "Result: ${api.call()}"
})
// Obtention du résultat (bloque le thread)
val result = future.get(5, TimeUnit.SECONDS)

// Arrêt du pool
fixedPool.shutdownNow()

CoroutineDispatcher comme thread pool

Les coroutines Kotlin fournissent CoroutineDispatcher — une abstraction similaire au thread pool. Dispatchers.IO utilise un pool de 64 threads (limité). Dispatchers.Default — un pool égal au nombre de cœurs CPU. CoroutineDispatcher ne nécessite pas d'arrêt manuel et est géré automatiquement. Pour un réglage fin, créez un ExecutorCoroutineDispatcher personnalisé via Executors.newFixedThreadPool(2).asCoroutineDispatcher(). Les coroutines ne remplacent pas le thread pool, mais l'encapsulent.

Thread Pool sous iOS : OperationQueue et GCD

iOS fournit deux mécanismes principaux pour gérer le thread pool : OperationQueue (API de haut niveau basée sur GCD) et GCD DispatchQueue (API de bas niveau en C). OperationQueue encapsule un thread pool via la propriété maxConcurrentOperationCount. Par défaut, OperationQueue utilise un maximum défini par le système (dépend de la charge système). DispatchQueue.global() fournit une file d'attente concurrente avec un pool de threads système.

OperationQueue et maxConcurrentOperationCount

OperationQueue gère le pool de threads via maxConcurrentOperationCount. La valeur 1 crée une file d'attente sérialisée (analogue à un pool à thread unique). Une valeur supérieure à 1 crée un pool concurrent avec la limite spécifiée. Par défaut, maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount (optimum système, généralement 4-8 threads). Operation prend en charge les dépendances, les priorités et l'annulation. Chaque opération s'exécute sur n'importe quel thread disponible du pool système.

swift
// OperationQueue avec un pool de 3 threads
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 3
queue.qualityOfService = .utility

// Création d'opérations
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) }
}

// Dépendance : operation2 attend operation1
operation2.addDependency(operation1)

// Ajout à la file
queue.addOperations([operation1, operation2], waitUntilFinished: false)

// Annulation de toutes les opérations
queue.cancelAllOperations()

GCD DispatchQueue comme thread pool

DispatchQueue — est le pool de threads d'Apple. Une file d'attente concurrente (qos: .utility) utilise le pool de threads système, optimisé pour la charge actuelle de l'appareil. Différents niveaux de QoS (userInteractive, userInitiated, utility, background) correspondent à différents pools avec différentes priorités. DispatchGroup permet de synchroniser plusieurs tâches. DispatchWorkItem prend en charge l'annulation et qualityOfService. Pour un contrôle précis, créez des files d'attente concurrentes personnalisées via DispatchQueue(label: qos: attributes: .concurrent).

swift
// GCD DispatchQueue comme thread pool
let customQueue = DispatchQueue(
    label: "com.app.background",
    qos: .utility,
    attributes: .concurrent,
    autoreleaseFrequency: .workItem
)

// Soumission des tâches au pool
customQueue.async { self.processFile(file1) }
customQueue.async { self.processFile(file2) }

// DispatchGroup pour synchronisation
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() // Les deux tâches terminées
}

// Limitation de la concurrence via sémaphore
let semaphore = DispatchSemaphore(value: 3)
for url in urls {
    pool.async {
        semaphore.wait()
        download(url)
        semaphore.signal()
    }
}

Paramètres de configuration de Thread Pool

La configuration du thread pool affecte directement les performances de l'application. Des paramètres incorrects entraînent une sous-utilisation du CPU (trop peu de threads) ou une surcharge du système (trop nombreux). Pour les applications mobiles, les valeurs optimales diffèrent de celles du côté serveur en raison des ressources limitées et de la consommation d'énergie. Les principaux paramètres sont : corePoolSize, maxPoolSize, capacité de la file d'attente et keepAliveTime.

Calcul de la taille optimale du pool

Formule pour les tâches IO-bound : corePoolSize = nombre de cœurs CPU × 2 (les threads attendent les E/S). Pour les tâches CPU-bound : corePoolSize = nombre de cœurs CPU (les threads sont constamment occupés par des calculs). Sur les appareils mobiles modernes (6-8 cœurs), cela donne 6-8 threads pour le CPU-bound et 12-16 pour l'IO-bound. Les tests pratiques montrent que pour une application mobile typique, 3-4 threads sont optimaux — plus de threads augmentent la consommation d'énergie sans gain de performances.

Capacité de la file d'attente et comportement de débordement

La taille de la file d'attente de travail (work queue) détermine combien de tâches peuvent attendre leur exécution. File illimitée (LinkedBlockingQueue sans limite) peut entraîner un OOM en cas d'arrivée rapide de tâches. File limitée (ArrayBlockingQueue de taille fixe) rejette les tâches lorsqu'elle est pleine. Pour les applications mobiles, ArrayBlockingQueue d'une capacité de 16-32 tâches est recommandée. CallerRunsPolicy est le meilleur RejectedExecutionHandler pour les mobiles : il ralentit l'appelant (contre-pression) plutôt que de perdre la tâche.

ParamètreRecommandation mobileJustification
corePoolSize2-4Ressources limitées de l'appareil mobile
maxPoolSizecorePoolSize (ou +1-2)Éviter les pics de charge de création de threads
keepAliveTime15-30 secondesLibération rapide de mémoire sans création fréquente
Capacité de file16-32Équilibre entre mise en mémoire tampon et risque OOM
HandlerCallerRunsPolicyContre-pression sans perte de tâches

Erreurs courantes lors du travail avec le pool de threads

Les développeurs d'applications mobiles commettent souvent des erreurs lors de l'utilisation du thread pool qui entraînent des plantages, des fuites mémoire et un fonctionnement instable. Les plus courantes : ne pas appeler shutdown() pour ExecutorService, créer un nouveau pool pour chaque opération, un pool trop grand, un deadlock entre tâches, utiliser CachedThreadPool sous Android.

Deadlock dans Thread Pool

Un deadlock se produit lorsqu'une tâche dans le pool attend le résultat d'une autre tâche du même pool, mais que tous les threads sont occupés à attendre. Exemple : la tâche A soumet la tâche B au même pool et appelle future.get() — si le pool est épuisé, la tâche A attend la tâche B, et la tâche B ne peut pas s'exécuter car il n'y a pas de threads libres. Solution : utilisez des pools séparés pour différents niveaux de tâches ou des callbacks asynchrones au lieu de .get() bloquant.

kotlin
// Deadlock dans Thread Pool
val pool = Executors.newFixedThreadPool(1)

// La tâche A attend la tâche B — deadlock !
val futureA = pool.submit {
    // Cette tâche ne s'exécutera jamais
    val futureB = pool.submit { 42 }
    futureB.get() // Se bloque pour toujours
}

// Solution : pools séparés
val workerPool = Executors.newFixedThreadPool(2)
val callbackPool = Executors.newSingleThreadExecutor()

workerPool.submit {
    callbackPool.submit {
        // Exécuté dans un pool séparé — deadlock impossible
    }
}

// Ou utilisez CompletableFuture
workerPool.submit {
    CompletableFuture
        .supplyAsync { 42 }
        .thenAccept { result ->
            println(result)
        }
}

Pool non arrêté et fuites

Un ExecutorService créé dans une Activity doit être arrêté dans onDestroy(). Si cela n'est pas fait, les threads resteront en mémoire même après la destruction de l'Activity. Solution : conservez le pool dans la portée de l'Application ou du ViewModel, pas dans l'Activity. Pour les coroutines, utilisez viewModelScope ou lifecycleScope. Si le pool est créé dans une Activity, assurez-vous d'appeler pool.shutdown() dans onDestroy(). Pour les tests, utilisez es.shutdownNow() pour un arrêt immédiat.

Questions fréquentes

En quoi Thread Pool diffère-t-il d'un thread normal ?

Thread Pool réutilise des threads déjà créés pour exécuter plusieurs tâches. Un thread normal (raw Thread) est créé, exécute une tâche et est détruit. La création d'un thread prend environ 1 Mo de mémoire et ~1 ms de temps. Thread Pool réduit les frais généraux, limite le nombre maximum de threads et fournit une API de gestion (shutdown, awaitTermination).

Combien de threads doit contenir un pool pour une application mobile ?

Pour une application mobile typique, 2-4 threads est optimal. Pour les tâches CPU-bound — le nombre de cœurs CPU. Pour les tâches IO-bound — nombre de cœurs × 2. Plus de threads augmentent la consommation d'énergie et les changements de contexte sans gain de performances. Sous Android, utilisez Process.availableProcessors() pour déterminer les cœurs. Sous iOS — ProcessInfo.processInfo.processorCount.

Qu'est-ce que CachedThreadPool et pourquoi est-il dangereux sous Android ?

CachedThreadPool crée des threads selon les besoins et réutilise les existants. Le problème : il ne limite pas le nombre maximum de threads. Si 100 tâches arrivent simultanément, 100 threads seront créés. Cela entraîne un OOM sous Android (chaque thread ~1 Mo). Utilisez newFixedThreadPool(n) avec une limite explicite. CachedThreadPool n'est acceptable que pour des tâches courtes en rafale avec un volume réduit garanti.

Faut-il appeler shutdown() pour ExecutorService ?

Oui, si le pool n'appartient pas à un conteneur géré (comme les coroutines). shutdown() arrête l'acceptation de nouvelles tâches et termine les threads après l'achèvement des tâches en cours. Sans shutdown(), les threads restent en mémoire et l'application ne se termine pas. Pour une Activity, appelez-le dans onDestroy(). Pour un ViewModel, utilisez coroutineScope. L'arrêt du pool est une partie obligatoire de la gestion des ressources, similaire à la fermeture d'un Cursor ou d'un InputStream.

OperationQueue et DispatchQueue sont-ils des pools de threads ?

Oui, OperationQueue et DispatchQueue sont des pools de threads fournis par iOS. OperationQueue limite la concurrence via maxConcurrentOperationCount. DispatchQueue.global() utilise le pool de threads système sans contrôle direct. Contrairement à Java ThreadPoolExecutor, vous ne gérez pas corePoolSize ni la capacité de la file — le système optimise le pool automatiquement en fonction de la charge actuelle et de la consommation d'énergie de l'appareil.

Résumé

  • Thread Pool — un pool de threads réutilisables pour les tâches en arrière-plan, réduisant les frais généraux de création de threads.
  • Android utilise ThreadPoolExecutor et Executors.newFixedThreadPool(n) avec une limite explicite de taille de pool.
  • iOS fournit OperationQueue avec maxConcurrentOperationCount et GCD DispatchQueue avec des pools QoS.
  • Core pool size — le nombre minimum de threads ; maximum pool size — le maximum lors du débordement de la file.
  • Deadlock dans le pool se produit lorsqu'une tâche se bloque en attendant une autre tâche du même pool.
  • CallerRunsPolicy est préférable pour les applications mobiles — il ralentit l'appelant sans perdre de tâches.
  • Pour une application mobile typique, la taille optimale du pool est de 2-4 threads avec shutdown() pour le nettoyage.

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