Heisenbug: cos’è, perché si verifica e metodi di cattura

Autore: IT Sectr Pubblicato: 2026-07-29 Tempo di lettura: 10 min

Heisenbug — un bug che scompare quando si prova a eseguirne il debug. Il termine deriva dal principio di indeterminazione di Heisenberg: l’osservazione influenza il comportamento del sistema. Nello sviluppo mobile, Heisenbug è uno dei problemi più difficili perché i metodi standard di debug (log, breakpoint, codice aggiuntivo) modificano lo stato del programma e nascondono il bug. Esploriamo le cause e i metodi per affrontare gli errori sfuggenti.

Punti Chiave

  • Race condition — la causa principale di Heisenbug: modificare il timing durante il debug maschera il problema
  • Bohrbug — un bug prevedibile, facilmente riproducibile a differenza di Heisenbug
  • Mandelbug — un bug con relazioni causali complesse, sensibile alle condizioni iniziali
  • ThreadSanitizer — uno strumento per rilevare le race condition senza influenzare il timing
  • Test deterministici — l’unico modo affidabile per riprodurre un Heisenbug

Cos’è un Heisenbug nello sviluppo mobile?

Heisenbug — una classe di errori che si manifestano in produzione o durante il normale funzionamento, ma scompaiono quando si tenta di riprodurli in un ambiente di debug. Il termine è stato coniato negli anni ’80 dal programmatore Jim Gray nel contesto dei sistemi distribuiti, ma oggi è più rilevante per le applicazioni mobili a causa della loro natura asincrona.

Il motivo principale: gli strumenti di debug standard modificano l’ambiente di esecuzione. Un breakpoint mette in pausa il thread per diversi millisecondi, il logging aggiunge I/O sincrono, controlli aggiuntivi cambiano l’ordine delle operazioni. In un ambiente multithread, anche un ritardo di microsecondi può alterare l’ordine di esecuzione dei thread e nascondere una race condition.

Secondo Microsoft Research (2022), circa il 15-25% di tutti i bug nelle applicazioni mobili multithread sono classificati come Heisenbug. Allo stesso tempo, il tempo per trovare e correggere un Heisenbug è in media 5-10 volte maggiore rispetto a un bug normale, a causa dell’impossibilità di riproduzione diretta.

Esempio di Heisenbug

Un’app si blocca in produzione durante lo scorrimento rapido di un elenco, ma quando ci si connette al debugger o si aggiungono log — funziona perfettamente. Causa: una race condition tra il thread UI (aggiornamento di RecyclerView) e il thread in background (aggiornamento dei dati dell’adattatore). I log aggiungono un ritardo che sincronizza i thread casualmente.

Bohrbug, Mandelbug, Heisenbug: Classificazione dei bug

Bohrbug — un bug prevedibile, stabilmente riproducibile. Chiamato per analogia con il modello atomico di Bohr: come un atomo, il bug si comporta allo stesso modo ogni volta che viene osservato. Esempio: NullPointerException quando si fa clic su un pulsante prima del caricamento dei dati. Trattato con test unitari standard.

Mandelbug — un bug con una relazione causale complessa e caotica (chiamato per analogia con l’insieme di Mandelbrot). Si manifesta solo sotto una certa combinazione di condizioni: versione del sistema operativo, modello del dispositivo, stato della rete. Si differenzia da Heisenbug perché non scompare durante il debug — il problema è la difficoltà di riproduzione, non il cambiamento di comportamento dovuto agli strumenti.

Heisenbug — un bug che scompare precisamente a causa degli strumenti di debug. Se aggiungi un log — il bug scompare. Se metti un breakpoint — il bug non si manifesta. Se rimuovi tutto — il bug torna. La causa principale: timing alterato durante il debug.

TipoRiproducibilitàReazione al debugEsempio
Bohrbug100%InvariataNPE su lista vuota
MandelbugCaoticaInvariataCrash su Android 12, Samsung, batteria scarica
HeisenbugSolo senza debugScompareRace condition che scompare con i log
SchrödinbugNon si manifesta nel codiceAppare guardandoloBug visibile nel codice ma mai attivato

Cause principali di Heisenbug

Race condition — la causa numero uno di Heisenbug. Due thread accedono a dati condivisi senza sincronizzazione. Il debugger introduce un ritardo, facendo sì che i thread si sincronizzino naturalmente. Senza il debugger, l’ordine di esecuzione è imprevedibile.

Errori dipendenti dal timing — bug che si manifestano solo a una certa velocità di esecuzione. Ad esempio, un’animazione che deve terminare prima dell’inizio dell’operazione successiva. Nel debugger, l’animazione è più lenta e l’operazione ha il tempo di iniziare dopo la fine dell’animazione. In produzione — il contrario.

kotlin
// Example race condition — typical Heisenbug
class ListViewModel : ViewModel() {
    private var items = mutableListOf<String>()

    fun loadFromNetwork() {
        viewModelScope.launch(Dispatchers.IO) {
            val result = api.fetchItems()
            items.addAll(result) // ❌ Not thread-safe
        }
    }

    fun getItems(): List<String> = items.toList()
    // Race condition: getItems read may overlap with loadFromNetwork write
}

Ottimizzazione del compilatore — il compilatore (JIT, ART, Kotlin/Native) può riordinare le istruzioni per l’ottimizzazione. In una build di debug, le ottimizzazioni sono disabilitate e il codice viene eseguito «come scritto». In una build di release, il compilatore modifica l’ordine delle operazioni, il che può rivelare presupposti nascosti nel codice.

  • ThreadLocal — uso errato di variabili thread-local che non sono visibili ad altri thread
  • Variabili non inizializzate — codice che si affida ai valori predefiniti dei campi della classe
  • Code GCD/dispatch — in iOS, ordine indefinito di esecuzione dei blocchi nelle code concorrenti
  • I/O bufferizzato — i dati non vengono scritti su disco fino a quando il buffer non è pieno

Strategie per catturare i bug sfuggenti

ThreadSanitizer (TSan) — uno strumento di Google per rilevare le race condition in C/C++ e Kotlin/Native. Viene incorporato nella build e rileva qualsiasi accesso alla memoria condivisa senza sincronizzazione. A differenza dei log, TSan non influisce sul timing perché funziona attraverso codice strumentato, non tramite I/O.

Test deterministici — sostituisci l’asincronia reale con asincronia controllata. Usa TestDispatcher (Kotlin), RxJava Plugins o code di test GCD (iOS) per il controllo completo sull’ordine di esecuzione. Specifica scenari concreti: il thread A viene eseguito, poi B, poi di nuovo A.

Logging ciclico — registrazione in un buffer circolare in memoria (non su disco). Quando si verifica il bug, il buffer viene salvato in un file. Poiché scrivere in memoria richiede nanosecondi (invece di millisecondi per I/O su disco), tale registrazione non influisce sul timing e non maschera l’Heisenbug.

kotlin
class CyclicBuffer(val capacity: Int = 1000) {
    private val buffer = ArrayDeque<String>(capacity)
    private val lock = Any()

    fun log(message: String) {
        synchronized(lock) {
            if (buffer.size >= capacity) buffer.removeFirst()
            buffer.addLast(message)
        }
    }

    fun flush() {
        synchronized(lock) { buffer.forEach { fileWriter.write(it) } }
    }
}

Logging in produzione — se il bug non è riproducibile localmente, raccogli dati in produzione. Usa log di Firebase Crashlytics, Sentry Breadcrumbs o un logger ciclico personalizzato. Importante: il logging deve essere asincrono e avere un impatto minimo sulle prestazioni.

Prevenzione di Heisenbug a livello di architettura

Isolamento dello stato — minimizza lo stato mutabile condiviso. Ogni componente dovrebbe avere il proprio stato isolato, inaccessibile per scrittura diretta da altri componenti. Usa Unidirectional Data Flow (UDF) — lo stato scorre in una direzione: Evento → Riduttore → Stato → UI.

Approccio funzionale — le funzioni pure senza effetti collaterali sono più facili da testare e debuggare. Isola gli effetti collaterali (rete, DB, file) in layer strettamente definiti (repository, data source). Gli errori di threading nel codice funzionale sono praticamente impossibili.

Modalità rigorosa — attiva Android StrictMode nella build di debug. Rileva violazioni della politica di threading (rete sul thread principale, I/O su disco sul thread principale) e lancia un’eccezione. Questo trasforma un potenziale Heisenbug in un Bohrbug deterministico immediatamente visibile.

kotlin
class DebugApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG) {
            StrictMode.setThreadPolicy(
                StrictMode.ThreadPolicy.Builder()
                    .detectDiskReads()
                    .detectDiskWrites()
                    .detectNetwork()
                    .penaltyLog()
                    .build()
            )
        }
    }
}

Revisione del codice con focus sull’asincronia — una parte obbligatoria del processo. Ogni pull request dovrebbe essere verificata per stato mutabile condiviso, collezioni non thread-safe e mancanza di sincronizzazione. Usa regole lint per vietare automaticamente determinati pattern (ad esempio, accesso a MutableList senza synchronized).

Domande Frequenti

Perché Heisenbug è così difficile da trovare?

Perché i metodi standard — breakpoint, log, print — modificano l’ambiente di esecuzione al punto che il bug smette di manifestarsi. Il debugger mette in pausa tutti i thread per decine di millisecondi. Durante questo tempo, la race condition che causava il bug si risolve naturalmente. Sono necessari strumenti che non influenzino il timing di esecuzione.

In che cosa Heisenbug si differenzia da Mandelbug?

Mandelbug è difficile da riprodurre a causa della complessità delle condizioni, ma gli strumenti di debug non influenzano la sua manifestazione. Heisenbug invece scompare proprio a causa degli strumenti di debug. Esempio di Mandelbug: un crash solo su dispositivi con Android 11, 3 GB di RAM e livello della batteria inferiore al 15%. Esempio di Heisenbug: una race condition che scompare aggiungendo Log.d().

Come testare Heisenbug in CI/CD?

Usa il rilevamento di test flaky — test che a volte falliscono, a volte passano. In Android, usa Android Test Orchestrator per isolare i test. Aggiungi StrictMode ai test di debug. Strumenta la build con ThreadSanitizer. Se un test è flaky >5% delle esecuzioni — consideralo un potenziale Heisenbug e indaga prima del merge.

Flow/Coroutines aiutano a evitare Heisenbug?

Parzialmente. Flow e la concorrenza strutturata in Kotlin riducono la quantità di stato mutabile condiviso e semplificano la gestione dei thread. Ma le coroutine non garantiscono la thread safety: se due coroutine condividono lo stato, una race condition è ancora possibile. Usa Mutex per proteggere lo stato condiviso o Channel per passare dati tra coroutine.

Cosa fare se Heisenbug si manifesta solo in produzione?

Usa un buffer di log ciclico in memoria con svuotamento automatico in caso di errore. Aggiungi monitoraggio dettagliato tramite Crashlytics o Sentry con breadcrumbs personalizzati. Per Android, attiva il rilevamento ANR e controlla i trace. Se il bug è una race condition, ThreadSanitizer in una build di debug con carico simile alla produzione può rivelare il problema.

Riepilogo

  • Heisenbug — un bug che scompare durante il debug; la causa principale sono i cambi di timing degli strumenti dello sviluppatore
  • Race condition — la causa principale di Heisenbug nelle applicazioni mobili, specialmente nel codice asincrono
  • Bohrbug (100% riproducibile) e Mandelbug (caotico) — altri tipi di bug, da non confondere con Heisenbug
  • ThreadSanitizer — il miglior strumento per rilevare le race condition, senza influenzare il timing di esecuzione
  • Logging ciclico in memoria invece che su disco — un modo per raccogliere dati senza mascherare Heisenbug
  • Unidirectional Data Flow e minimizzazione dello stato mutabile condiviso — prevenzione architetturale di un’intera classe di errori
  • StrictMode nelle build di debug trasforma un potenziale Heisenbug in un Bohrbug deterministico, immediatamente visibile

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