Background Thread — un thread d'exécution non lié à l'interface utilisateur, conçu pour les opérations longues : requêtes réseau, travail avec les fichiers, analyse JSON, compression d'images, chiffrement et requêtes de base de données. Sous iOS, les threads d'arrière-plan sont gérés via GCD (DispatchQueue.global) et OperationQueue ; sous Android, via Executors, WorkManager et Kotlin Coroutines (Dispatchers.IO, Dispatchers.Default). Selon la Documentation Apple DispatchQueue, après la fin d'une opération en arrière-plan, le résultat doit être retourné au Main Thread pour mettre à jour l'interface.
Points clés
Background Thread — tout thread dans une application qui n'est pas le Main Thread et n'a pas accès à l'UI. Sa tâche est de libérer le thread principal des opérations lourdes pour que l'interface reste réactive. Le système d'exploitation répartit les threads d'arrière-plan sur les cœurs du processeur, permettant d'exécuter plusieurs tâches en parallèle. iOS gère automatiquement le pool de threads via GCD, Android via les pools Java Executors.
Contrairement au Main Thread qui traite les événements séquentiellement (un par un), les threads d'arrière-plan peuvent s'exécuter en parallèle, limités uniquement par le nombre de cœurs CPU. Par exemple, sur un appareil à 8 cœurs, on peut lancer jusqu'à 8 tâches d'arrière-plan parallèles sans ralentissement significatif. Cependant, un nombre excessif de threads (centaines) conduit à thread starvation — compétition pour les cœurs et augmentation des frais généraux de changement de contexte (context switch).
Quality of Service (QoS) — mécanisme iOS qui permet de spécifier la priorité d'une tâche d'arrière-plan. Valeurs : .userInteractive (la plus élevée, presque Main Thread), .userInitiated (l'utilisateur attend un résultat), .default (standard), .utility (l'utilisateur n'attend pas directement), .background (la plus basse, pour synchronisation et indexation). Sous Android, l'équivalent est Thread.setPriority() de 1 à 10, mais Android utilise également cgroups pour la gestion groupée des priorités des threads.
DispatchQueue.global(qos:) — le moyen principal d'obtenir une file d'attente d'arrière-plan sous iOS. GCD (Grand Central Dispatch) crée automatiquement un pool de threads et répartit les tâches sur les cœurs. L'appel DispatchQueue.global(qos: .background).async {} envoie un bloc dans la file d'attente d'arrière-plan avec la priorité la plus basse. Pour les tâches dont le résultat est nécessaire immédiatement, utilisez .userInitiated ou .utility.
OperationQueue — une abstraction de plus haut niveau sur GCD qui permet de définir des dépendances entre opérations, le nombre maximum d'opérations simultanées (maxConcurrentOperationCount) et les priorités. OperationQueue est pratique pour les chaînes multitâches complexes : télécharger fichier → décompresser → sauvegarder en cache. Par défaut, OperationQueue utilise les threads d'arrière-plan sauf indication contraire.
import UIKit
class ImageDownloader {
func downloadImagesSequentially() {
let urls = ["https://example.com/1.png", "https://example.com/2.png"]
// OperationQueue avec 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("Téléchargé : \(url.lastPathComponent)")
}
}
}
}
// GCD : file d'attente globale d'arrière-plan avec différentes QoS
func backgroundTaskWithQoS() {
DispatchQueue.global(qos: .userInitiated).async {
// Priorité élevée — l'utilisateur attend le résultat
let result = self.heavyComputation()
DispatchQueue.main.async {
self.showResult(result)
}
}
}
private func heavyComputation() -> String {
Thread.sleep(forTimeInterval: 2) // simulation de travail
return "Résultat du calcul"
}
private func showResult(_ result: String) {
print("Result on Main: \(result)")
}
}
Dans l'exemple, OperationQueue charge deux images en parallèle (maxConcurrentOperationCount = 2) avec QoS d'arrière-plan via qualityOfService = .utility. La méthode GCD backgroundTaskWithQoS utilise une file d'attente globale avec .userInitiated pour une tâche dont l'utilisateur attend le résultat. Les deux approches se terminent en retournant sur DispatchQueue.main pour mettre à jour l'UI — c'est une exigence obligatoire sous iOS.
GCD prend en charge deux types de files d'attente : séries et concurrentes. Les files séries exécutent les tâches une par une — c'est pratique pour accéder à une ressource partagée (fichier, BD) sans verrous. Les files concurrentes exécutent les tâches en parallèle, les répartissant sur les cœurs disponibles. DispatchQueue.global est toujours concurrente. Pour créer une file série, utilisez DispatchQueue(label: "com.app.queue").
Android propose plusieurs niveaux d'abstraction pour les threads d'arrière-plan. L'approche classique est java.util.concurrent.Executors.newFixedThreadPool(n) ou Executors.newCachedThreadPool(). L'approche moderne est Kotlin Coroutines avec Dispatchers.IO (pour les E/S : réseau, fichiers, BD) et Dispatchers.Default (pour les tâches intensives en CPU : tri, traitement d'images). WorkManager est pour les tâches d'arrière-plan différées et garanties.
HandlerThread — une classe spécialisée Android pour créer un thread d'arrière-plan avec son propre Looper (file de messages). Contrairement à Executors, HandlerThread permet d'envoyer des messages et des Runnables via un Handler. Il est utilisé pour les opérations qui nécessitent une mise en file d'attente (par exemple, écriture séquentielle en BD). Après utilisation, quit() ou quitSafely() doit être appelé pour libérer les ressources.
// Android : Executors et Dispatchers de Coroutines
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.concurrent.Executors
class DataRepository {
private val ioExecutor = Executors.newFixedThreadPool(4)
// Approche classique via Executors
fun loadDataLegacy(callback: (String) -> Unit) {
ioExecutor.execute {
val result = readFromFile()
val handler = android.os.Handler(android.os.Looper.getMainLooper())
handler.post { callback(result) }
}
}
// Approche moderne via Coroutines
suspend fun loadDataCoroutines(): String {
return withContext(Dispatchers.IO) {
// Opération fichier — exécution dans le pool d'arrière-plan
readFromFile()
}
// Résultat retourné automatiquement à Dispatchers.Main
}
// Tâche intensive en CPU sur Dispatchers.Default
suspend fun processImage(pixels: IntArray): IntArray {
return withContext(Dispatchers.Default) {
// Tri, filtrage — exécution sur le pool Default
pixels.sortedArray()
}
}
private fun readFromFile(): String {
Thread.sleep(1000) // simulation de lecture de fichier
return "file_content"
}
fun cleanup() {
ioExecutor.shutdown()
}
}
L'exemple DataRepository montre l'évolution des threads d'arrière-plan sous Android. La méthode legacy loadDataLegacy utilise Executors.newFixedThreadPool(4) avec un Handler pour retourner au Main Thread. La méthode moderne loadDataCoroutines utilise withContext(Dispatchers.IO) — la coroutine est suspendue pendant l'exécution sans bloquer le thread et reprend automatiquement sur le Main Thread. Dispatchers.Default est recommandé pour les opérations intensives en CPU (tri, filtrage, transformation de données).
Kotlin Coroutines — pas seulement une façon de travailler avec les threads, mais un modèle fondamentalement différent : les tâches asynchrones ne sont pas liées à un thread spécifique et peuvent être suspendues sans bloquer. Cela signifie qu'en arrière-plan, une coroutine n'occupe pas un thread mais le libère pour d'autres tâches. Le mécanisme de suspension permet d'exécuter des centaines de milliers de tâches concurrentes sur un pool de 4–8 threads sans thread starvation.
Trois dispatchers principaux : Dispatchers.Main (UI, un thread), Dispatchers.IO (64 threads par défaut pour les opérations bloquantes : réseau, fichiers, BD), Dispatchers.Default (égal au nombre de cœurs CPU, pour les calculs intensifs). En les combinant via withContext, le développeur change de thread sans créer de callbacks. withContext est une fonction suspend qui ne rend pas le contrôle tant que la tâche n'est pas terminée.
// Coroutines : composition de tâches d'arrière-plan
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
suspend fun loadUserProfile(userId: String): UserProfile =
coroutineScope {
// Chargement parallèle de données depuis différentes sources
val user = async(Dispatchers.IO) { fetchUser(userId) }
val posts = async(Dispatchers.IO) { fetchPosts(userId) }
val avatar = async(Dispatchers.Default) {
processAvatar(fetchAvatar(userId))
}
// await() — suspend jusqu'à la fin de toutes les tâches
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 }
La fonction loadUserProfile lance trois tâches d'arrière-plan parallèles via async. fetchUser et fetchPosts sont liées aux E/S (réseau), exécutées sur Dispatchers.IO. processAvatar est intensive en CPU (traitement d'image), exécutée sur Dispatchers.Default. await() suspend la coroutine jusqu'à la fin de toutes les tâches. Le temps d'exécution total est égal au temps maximum parmi les trois tâches (500 ms pour fetchPosts), et non à leur somme. C'est un avantage clé des coroutines sur l'exécution séquentielle.
Structured concurrency — principe selon lequel chaque coroutine a une portée parente, et l'annulation du parent annule automatiquement les coroutines enfants. Sous Android, lifecycleScope annule toutes les coroutines lors de la destruction de l'Activity. viewModelScope le fait lors du nettoyage du ViewModel. Cela évite les fuites de tâches d'arrière-plan : si l'utilisateur ferme l'écran, en arrière-plan la coroutine ne continuera pas à charger des données qui ne sont plus nécessaires.
WorkManager — une bibliothèque Android Jetpack pour exécuter des tâches d'arrière-plan qui doivent être accomplies même après un redémarrage de l'appareil ou la fermeture de l'application. Contrairement à Executors et aux coroutines, qui vivent dans le processus de l'application, WorkManager confie la tâche à un dispatcher système qui garantit l'exécution dans des conditions appropriées (disponibilité réseau, charge de la batterie, espace libre). WorkManager est adapté à la synchronisation de données, l'envoi de logs et la sauvegarde.
Une tâche dans WorkManager est une classe qui étend Worker (ou CoroutineWorker pour les coroutines). Worker.doWork() s'exécute sur un thread d'arrière-plan fourni par WorkManager. Le résultat est retourné via Result.success(), Result.retry() ou Result.failure(). Les tâches peuvent être chaînées : oneTimeWorkRequest.andThen(nextRequest).enqueue(). WorkManager choisit lui-même le moment d'exécution optimal en tenant compte des contraintes (Constraints).
// WorkManager avec 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 {
// Exécution sur Dispatchers.Default (par défaut)
return withContext(Dispatchers.IO) {
try {
syncDataToServer()
Result.success()
} catch (e: Exception) {
if (runAttemptCount < 3) Result.retry() else Result.failure()
}
}
}
private suspend fun syncDataToServer() {
// Simulation de synchronisation
delay(1000)
}
}
// Lancement de tâche WorkManager avec contraintes
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 étend CoroutineWorker — une version de Worker prenant en charge les coroutines. doWork() s'exécute sur Dispatchers.Default, avec basculement sur IO pour les opérations réseau via withContext. Les Constraints garantissent que la synchronisation ne démarre qu'en présence de réseau et d'une charge batterie non inférieure à un niveau bas. BackoffCriteria avec EXPONENTIAL augmente l'intervalle entre les tentatives : 10, 20, 40 secondes.
Pour les tâches régulières (synchronisation toutes les 15 minutes, envoi d'analyses toutes les heures), WorkManager fournit PeriodicWorkRequestBuilder. L'intervalle minimum est de 15 minutes. Contrairement à OneTimeWorkRequest, PeriodicWorkRequest ne garantit pas le respect exact de l'intervalle — le système peut regrouper plusieurs tâches périodiques pour économiser la batterie. Pour des intervalles précis, utilisez AlarmManager, mais tenez compte des restrictions d'Android 12+ sur les alarmes exactes.
Première erreur — créer un nouveau Thread pour chaque tâche. new Thread().start() crée un thread natif allouant ~1 Mo pour la pile. Pour 100 tâches parallèles, cela fait 100 Mo rien que pour les piles, plus les frais généraux de changement de contexte. Utilisez des pools de threads : Executors.newFixedThreadPool(n) (Android) ou DispatchQueue.global() (iOS) — ils réutilisent les threads, réduisant les frais généraux d'un ordre de grandeur.
Deuxième erreur — accéder à un état mutable depuis plusieurs threads d'arrière-plan sans synchronisation. Si deux threads d'arrière-plan écrivent simultanément dans le même ArrayList ou HashMap, des conditions de concurrence apparaissent : ConcurrentModificationException sous Android, corruption de données sous iOS. Solution : utilisez des collections thread-safe (ConcurrentHashMap, CopyOnWriteArrayList) ou sérialisez l'accès via une seule file d'attente (DispatchQueue serial).
Troisième erreur — des tâches d'arrière-plan sans gestion de cycle de vie. Lancer une coroutine dans une portée globale sans la lier au cycle de vie de l'Activity ou du ViewModel entraîne des fuites : la tâche continue de s'exécuter après la destruction de l'écran. Sous Android, utilisez lifecycleScope (Activity/Fragment) ou viewModelScope (ViewModel). Sous iOS, utilisez weak self dans les closures et annulez les tâches lors du deinit.
Questions fréquentes
Background Thread — un thread sur lequel sont exécutées les opérations non liées à l'UI : requêtes réseau, lecture/écriture de fichiers, analyse JSON, calculs. Il libère le Main Thread du travail lourd, préservant la réactivité de l'interface. Sous iOS, les threads d'arrière-plan sont gérés via GCD (DispatchQueue.global) ; sous Android, via Executors ou Kotlin Coroutines (Dispatchers.IO, Dispatchers.Default).
Dispatchers.IO est conçu pour les opérations d'E/S bloquantes : lecture de fichiers, requêtes réseau, travail avec la BD. Il a un pool de 64 threads. Dispatchers.Default est pour les tâches intensives en CPU : tri, filtrage, traitement d'images. Son pool est égal au nombre de cœurs CPU. Utiliser Dispatchers.Default pour des opérations d'E/S peut bloquer tous les cœurs, et Dispatchers.IO pour des tâches CPU peut créer un nombre excessif de threads.
DispatchQueue.global(qos: .background).async { } envoie un bloc dans la file d'attente globale d'arrière-plan. Après le travail d'arrière-plan, il faut revenir au thread principal via DispatchQueue.main.async { } pour mettre à jour l'UI. Pour les tâches séquentielles d'arrière-plan, utilisez OperationQueue avec maxConcurrentOperationCount = 1 ou DispatchQueue(label: "serial").
Le nombre recommandé de threads d'arrière-plan est égal au nombre de cœurs CPU plus 1 pour les tâches liées aux E/S. Sur un appareil moderne à 8 cœurs, cela donne 9 threads. Créer des centaines de threads entraîne thread starvation : l'OS passe plus de temps à changer de contexte qu'à exécuter des tâches. GCD sous iOS et Executors sous Android optimisent automatiquement le pool de threads pour l'appareil actuel.
Dans Kotlin Coroutines, le retour au Main Thread se fait automatiquement si la coroutine a été lancée dans un scope Main (lifecycleScope.launch, viewModelScope.launch). La fonction withContext(Dispatchers.IO) suspend la coroutine sur un thread IO et, après la fin, la reprend automatiquement sur le dispatcher où elle a été lancée (généralement Main). Un appel explicite à DispatchQueue.main.async n'est pas nécessaire.
Résumé
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.
Lisez aussi