Background Thread nello sviluppo mobile: cos'è, compiti e modalità d'uso

Autore: IT Sectr Pubblicato: 2026-03-15 Tempo di lettura: 11 min

Background Thread — un thread di esecuzione non legato all'interfaccia utente, progettato per operazioni di lunga durata: richieste di rete, lavoro con file, parsing JSON, compressione immagini, crittografia e query al database. Su iOS, i thread in background sono gestiti tramite GCD (DispatchQueue.global) e OperationQueue; su Android, tramite Executors, WorkManager e Kotlin Coroutines (Dispatchers.IO, Dispatchers.Default). Secondo la Documentazione Apple DispatchQueue, dopo il completamento di un'operazione in background, il risultato deve essere restituito al Main Thread per aggiornare l'interfaccia.

Punti chiave

  • Background Thread esegue operazioni che bloccano l'UI: rete, file, JSON, calcoli
  • iOS: DispatchQueue.global(qos:) e OperationQueue per attività in background
  • Android: Dispatchers.IO (rete/file), Dispatchers.Default (calcoli), WorkManager (attività in background)
  • Coroutine — lo standard moderno per il lavoro in background: withContext(Dispatchers.IO) cambia thread senza callback hell
  • Risultato dal Background Thread viene sempre restituito al Main Thread per aggiornare l'UI

Cos'è un Background Thread

Background Thread — qualsiasi thread in un'applicazione che non sia il Main Thread e non abbia accesso all'UI. Il suo compito è liberare il thread principale dalle operazioni pesanti per mantenere l'interfaccia reattiva. Il sistema operativo distribuisce i thread in background tra i core della CPU, consentendo di eseguire più attività in parallelo. iOS gestisce automaticamente il pool di thread tramite GCD, Android tramite i pool di Java Executors.

A differenza del Main Thread, che elabora gli eventi in sequenza (uno dopo l'altro), i thread in background possono essere eseguiti in parallelo, limitati solo dal numero di core della CPU. Ad esempio, su un dispositivo a 8 core è possibile eseguire fino a 8 attività in background parallele senza un significativo rallentamento. Tuttavia, un numero eccessivo di thread (centinaia) porta al thread starvation — competizione per i core e aumento del overhead del cambio di contesto (context switch).

Quality of Service (QoS) — meccanismo iOS che permette di specificare la priorità di un'attività in background. Valori: .userInteractive (il più alto, quasi Main Thread), .userInitiated (l'utente si aspetta un risultato), .default (standard), .utility (l'utente non attende direttamente), .background (il più basso, per sincronizzazione e indicizzazione). Su Android, l'equivalente è Thread.setPriority() da 1 a 10, ma Android utilizza anche cgroups per la gestione di gruppo delle priorità dei thread.

Background Thread su iOS: GCD e DispatchQueue.global

DispatchQueue.global(qos:) — il modo principale per ottenere una coda in background su iOS. GCD (Grand Central Dispatch) crea automaticamente un pool di thread e distribuisce le attività tra i core. La chiamata DispatchQueue.global(qos: .background).async {} invia un blocco alla coda in background con la priorità più bassa. Per attività il cui risultato è necessario immediatamente, utilizzare .userInitiated o .utility.

OperationQueue — un'astrazione di più alto livello su GCD che consente di impostare dipendenze tra operazioni, il numero massimo di operazioni simultanee (maxConcurrentOperationCount) e priorità. OperationQueue è utile per catene multitasking complesse: scaricare file → decomprimere → salvare nella cache. Per impostazione predefinita, OperationQueue utilizza thread in background, salvo diversa indicazione.

swift
import UIKit

class ImageDownloader {

    func downloadImagesSequentially() {
        let urls = ["https://example.com/1.png", "https://example.com/2.png"]

        // OperationQueue con 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("Caricato: \(url.lastPathComponent)")
                }
            }
        }
    }

    // GCD: coda globale in background con diversi QoS
    func backgroundTaskWithQoS() {
        DispatchQueue.global(qos: .userInitiated).async {
            // Priorità alta — l'utente attende il risultato
            let result = self.heavyComputation()
            DispatchQueue.main.async {
                self.showResult(result)
            }
        }
    }

    private func heavyComputation() -> String {
        Thread.sleep(forTimeInterval: 2) // simulazione di lavoro
        return "Risultato del calcolo"
    }

    private func showResult(_ result: String) {
        print("Result on Main: \(result)")
    }
}

Nell'esempio, OperationQueue carica due immagini in parallelo (maxConcurrentOperationCount = 2) con QoS in background tramite qualityOfService = .utility. Il metodo GCD backgroundTaskWithQoS utilizza una coda globale con .userInitiated per un'attività di cui l'utente attende il risultato. Entrambi gli approcci terminano tornando a DispatchQueue.main per aggiornare l'UI — questo è un requisito obbligatorio su iOS.

Code in background Serial vs Concurrent

GCD supporta due tipi di code: seriali e concorrenti. Le code seriali eseguono le attività una dopo l'altra — comodo per accedere a una risorsa condivisa (file, DB) senza blocchi. Le code concorrenti eseguono le attività in parallelo, distribuendole tra i core disponibili. DispatchQueue.global è sempre concorrente. Per creare una coda seriale, utilizzare DispatchQueue(label: "com.app.queue").

Background Thread su Android: Executors e Dispatchers

Android fornisce diversi livelli di astrazione per i thread in background. L'approccio classico è java.util.concurrent.Executors.newFixedThreadPool(n) o Executors.newCachedThreadPool(). L'approccio moderno sono le Kotlin Coroutines con Dispatchers.IO (per I/O: rete, file, DB) e Dispatchers.Default (per attività intensive di CPU: ordinamento, elaborazione immagini). WorkManager è per attività in background differite e garantite.

HandlerThread — una classe Android specializzata per creare un thread in background con il proprio Looper (coda di messaggi). A differenza di Executors, HandlerThread consente di inviare messaggi e Runnable tramite un Handler. Viene utilizzato per operazioni che richiedono l'accodamento (ad esempio, scrittura sequenziale nel DB). Dopo l'uso, è necessario chiamare quit() o quitSafely() per liberare le risorse.

kotlin
// Android: Executors e Dispatchers delle Coroutine
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.concurrent.Executors

class DataRepository {

    private val ioExecutor = Executors.newFixedThreadPool(4)

    // Approccio classico tramite Executors
    fun loadDataLegacy(callback: (String) -> Unit) {
        ioExecutor.execute {
            val result = readFromFile()
            val handler = android.os.Handler(android.os.Looper.getMainLooper())
            handler.post { callback(result) }
        }
    }

    // Approccio moderno tramite Coroutine
    suspend fun loadDataCoroutines(): String {
        return withContext(Dispatchers.IO) {
            // Operazione file — esecuzione nel pool in background
            readFromFile()
        }
        // Risultato restituito automaticamente a Dispatchers.Main
    }

    // Attività intensiva di CPU su Dispatchers.Default
    suspend fun processImage(pixels: IntArray): IntArray {
        return withContext(Dispatchers.Default) {
            // Ordinamento, filtraggio — esecuzione sul pool Default
            pixels.sortedArray()
        }
    }

    private fun readFromFile(): String {
        Thread.sleep(1000) // simulazione lettura da file
        return "file_content"
    }

    fun cleanup() {
        ioExecutor.shutdown()
    }
}

L'esempio DataRepository mostra l'evoluzione dei thread in background su Android. Il metodo legacy loadDataLegacy utilizza Executors.newFixedThreadPool(4) con un Handler per tornare al Main Thread. Il metodo moderno loadDataCoroutines utilizza withContext(Dispatchers.IO) — la coroutine viene sospesa durante l'esecuzione senza bloccare il thread e riprende automaticamente sul Main Thread. Dispatchers.Default è consigliato per operazioni CPU-bound (ordinamento, filtraggio, trasformazione dati).

Le Coroutine come standard moderno per attività in background

Kotlin Coroutines — non solo un modo di lavorare con i thread, ma un modello fondamentalmente diverso: le attività asincrone non sono legate a un thread specifico e possono essere sospese senza bloccare. Ciò significa che, mentre è in background, una coroutine non occupa un thread ma lo libera per altre attività. Il meccanismo di sospensione consente di eseguire centinaia di migliaia di attività concorrenti su un pool di 4–8 thread senza thread starvation.

Tre dispatcher principali: Dispatchers.Main (UI, un thread), Dispatchers.IO (64 thread per impostazione predefinita per operazioni bloccanti: rete, file, DB), Dispatchers.Default (uguale al numero di core CPU, per calcoli intensivi). Combinandoli tramite withContext, lo sviluppatore cambia thread senza creare callback. withContext è una funzione suspend che non restituisce il controllo fino al completamento dell'attività.

kotlin
// Coroutine: composizione di attività in background
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay

suspend fun loadUserProfile(userId: String): UserProfile =
    coroutineScope {
        // Caricamento parallelo di dati da diverse fonti
        val user = async(Dispatchers.IO) { fetchUser(userId) }
        val posts = async(Dispatchers.IO) { fetchPosts(userId) }
        val avatar = async(Dispatchers.Default) {
            processAvatar(fetchAvatar(userId))
        }

        // await() — si sospende fino al completamento di tutte le attività
        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 funzione loadUserProfile avvia tre attività in background parallele tramite async. fetchUser e fetchPosts sono IO-bound (rete), eseguite su Dispatchers.IO. processAvatar è CPU-bound (elaborazione immagini), eseguita su Dispatchers.Default. await() sospende la coroutine fino al completamento di tutte le attività. Il tempo totale di esecuzione è uguale al tempo massimo tra le tre attività (500 ms per fetchPosts), non alla loro somma. Questo è un vantaggio chiave delle coroutine rispetto all'esecuzione sequenziale.

Concorrenza strutturata: prevenzione delle perdite

Structured concurrency — principio per cui ogni coroutine ha un ambito padre e la cancellazione del padre annulla automaticamente le coroutine figlie. Su Android, lifecycleScope annulla tutte le coroutine alla distruzione dell'Activity. viewModelScope lo fa alla pulizia del ViewModel. Ciò previene le perdite di attività in background: se l'utente chiude lo schermo, mentre è in background la coroutine non continuerà a caricare dati che non sono più necessari.

WorkManager: attività in background per Android

WorkManager — una libreria Android Jetpack per eseguire attività in background che devono essere completate anche dopo il riavvio del dispositivo o la chiusura dell'app. A differenza di Executors e coroutine, che vivono all'interno del processo dell'app, WorkManager affida l'attività a un dispatcher di sistema che ne garantisce l'esecuzione in condizioni appropriate (disponibilità di rete, carica della batteria, spazio libero). WorkManager è adatto per sincronizzazione dati, caricamento log e backup.

Un'attività in WorkManager è una classe che estende Worker (o CoroutineWorker per le coroutine). Worker.doWork() viene eseguito su un thread in background fornito da WorkManager. Il risultato viene restituito tramite Result.success(), Result.retry() o Result.failure(). Le attività possono essere concatenate: oneTimeWorkRequest.andThen(nextRequest).enqueue(). WorkManager sceglie autonomamente il momento ottimale di esecuzione considerando i vincoli (Constraints).

kotlin
// WorkManager con coroutine
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 {
        // Esecuzione su Dispatchers.Default (per impostazione predefinita)
        return withContext(Dispatchers.IO) {
            try {
                syncDataToServer()
                Result.success()
            } catch (e: Exception) {
                if (runAttemptCount < 3) Result.retry() else Result.failure()
            }
        }
    }

    private suspend fun syncDataToServer() {
        // Simulazione sincronizzazione
        delay(1000)
    }
}

// Avvio attività WorkManager con vincoli
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 estende CoroutineWorker — una versione di Worker che supporta le coroutine. doWork() viene eseguito su Dispatchers.Default, con passaggio a IO per le operazioni di rete tramite withContext. I Constraints garantiscono che la sincronizzazione venga avviata solo in presenza di rete e con batteria non al di sotto di un livello basso. BackoffCriteria con EXPONENTIAL aumenta l'intervallo tra i tentativi: 10, 20, 40 secondi.

PeriodicWorkRequest per attività in background periodiche

Per attività periodiche (sincronizzazione ogni 15 minuti, invio di analisi ogni ora), WorkManager fornisce PeriodicWorkRequestBuilder. L'intervallo minimo è di 15 minuti. A differenza di OneTimeWorkRequest, PeriodicWorkRequest non garantisce il rispetto esatto dell'intervallo — il sistema può raggruppare più attività periodiche per risparmiare batteria. Per intervalli precisi, utilizzare AlarmManager, ma considerare le restrizioni di Android 12+ sugli allarmi esatti.

Errori tipici con i thread in background

Primo errore — creare un nuovo Thread per ogni attività. new Thread().start() crea un thread nativo che alloca ~1 MB per lo stack. Per 100 attività in parallelo, sono 100 MB solo per gli stack, più l'overhead del cambio di contesto. Utilizzare pool di thread: Executors.newFixedThreadPool(n) (Android) o DispatchQueue.global() (iOS) — riutilizzano i thread, riducendo l'overhead di ordini di grandezza.

Secondo errore — accedere a stato mutabile da più thread in background senza sincronizzazione. Se due thread in background scrivono simultaneamente nello stesso ArrayList o HashMap, si verificano race condition: ConcurrentModificationException su Android, corruzione dati su iOS. Soluzione: utilizzare collection thread-safe (ConcurrentHashMap, CopyOnWriteArrayList) o serializzare l'accesso tramite una singola coda (DispatchQueue serial).

Terzo errore — attività in background senza gestione del ciclo di vita. Avviare una coroutine in un ambito globale senza legarla al ciclo di vita di Activity o ViewModel causa perdite: l'attività continua a essere eseguita dopo la distruzione dello schermo. Su Android, utilizzare lifecycleScope (Activity/Fragment) o viewModelScope (ViewModel). Su iOS, utilizzare weak self nelle closure e annullare le attività al deinit.

Domande frequenti

Cos'è un Background Thread nelle applicazioni mobili?

Background Thread — un thread su cui vengono eseguite operazioni non legate all'UI: richieste di rete, lettura/scrittura di file, parsing JSON, calcoli. Libera il Main Thread dal lavoro pesante, mantenendo l'interfaccia reattiva. Su iOS, i thread in background sono gestiti tramite GCD (DispatchQueue.global); su Android, tramite Executors o Kotlin Coroutines (Dispatchers.IO, Dispatchers.Default).

Qual è la differenza tra Dispatchers.IO e Dispatchers.Default?

Dispatchers.IO è progettato per operazioni di I/O bloccanti: lettura di file, richieste di rete, lavoro con DB. Ha un pool di 64 thread. Dispatchers.Default è per attività intensive di CPU: ordinamento, filtraggio, elaborazione immagini. Il suo pool è uguale al numero di core CPU. Usare Dispatchers.Default per operazioni di I/O può bloccare tutti i core, e Dispatchers.IO per attività CPU può creare un numero eccessivo di thread.

Come passare a un thread in background su iOS?

DispatchQueue.global(qos: .background).async { } invia un blocco alla coda globale in background. Dopo il completamento del lavoro in background, è necessario tornare al thread principale tramite DispatchQueue.main.async { } per aggiornare l'UI. Per attività sequenziali in background, utilizzare OperationQueue con maxConcurrentOperationCount = 1 o DispatchQueue(label: "serial").

Quanti thread in background si possono creare in un'applicazione mobile?

Il numero raccomandato di thread in background è uguale al numero di core CPU più 1 per le attività IO-bound. Su un moderno dispositivo a 8 core, sono 9 thread. Creare centinaia di thread porta a thread starvation: il sistema operativo spende più tempo nel cambio di contesto che nell'esecuzione delle attività. GCD su iOS ed Executors su Android ottimizzano automaticamente il pool di thread per il dispositivo corrente.

È necessario tornare al Main Thread dopo una coroutine?

In Kotlin Coroutines, il ritorno al Main Thread avviene automaticamente se la coroutine è stata avviata in un ambito Main (lifecycleScope.launch, viewModelScope.launch). La funzione withContext(Dispatchers.IO) sospende la coroutine su un thread IO e, al completamento, la riprende automaticamente sul dispatcher in cui è stata avviata (di solito Main). Non è richiesta una chiamata esplicita a DispatchQueue.main.async.

Riepilogo

  • Background Thread — thread per operazioni che non dovrebbero essere eseguite sul Main Thread: rete, file, parsing JSON, calcoli
  • iOS: DispatchQueue.global(qos:) e OperationQueue sono le API principali per attività in background con supporto QoS
  • Android: Executors, HandlerThread, WorkManager per Java; Dispatchers.IO/Default + coroutine per Kotlin
  • Coroutine con withContext cambiano thread senza callback e senza blocco (meccanismo suspend)
  • WorkManager garantisce l'esecuzione di attività in background anche dopo il riavvio del dispositivo, rispettando i vincoli
  • Errori: creare nuovi Thread invece di usare un pool, race condition nell'accesso a stato mutabile, perdite per mancanza di legame al ciclo di vita
  • Risultato dal thread in background viene sempre restituito al Main Thread: tramite Dispatchers.Main (Android) o DispatchQueue.main.async (iOS)

Svilupperemo un'applicazione mobile chiavi in mano

IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.

Discuti il progetto

Leggi anche