Quando una elaborazione “pesante” viene eseguita in un contesto di una interfaccia utente, sia essa Winform, MFC o WPF, è necessario che questa venga “alimentata” servendo il thread sottostante:
- Windows Forms ed MFC, come anche altre librerie basate sulle Win32, hanno bisogno di alimentare la message pump. Si tratta di una coda associata ad uno specifico thread che accumula messaggi e li smista verso i controlli contenuti.
- WPF non ha message pump perché ha un meccanismo di rendering completamente differente. Nonostante questo esiste un meccanismo analogo alla message pump, basasto sul Dispatcher di WPF.
Fin qui tutto chiaro. Il developer attento conosce questi aspetti ma ci sono almeno due casi in cui queste precauzioni vengono tipicamente ignorate:
- Quando l’elaborazione viene eseguita in un thread differente, il developer è “tranquillo” di lasciare libero il dispatcher del thread principale e perciò di eseguire tutto nel rispetto delle regole
- Quando non esiste neppure una finestra di presentazione perché WPF viene usato per eseguire delle elaborazioni non visuali: creazione di XPS, rendering per la creazione di bitmap, etc.
Questi casi sono tipici per elaborazioni dentro servizi WCF o applicazioni asp.net
Da un punto di vista di elaborazione grafica tutto questo non fa una grinza eppure c’è un problema.
Tutto nasce da un test per verificare una procedura complessa per la creazione di documenti XPS. Ad ogni loop viene creato un documento e salvato su file. Una volta eseguito il giro, tutti gli oggetti grafici che sono serviti per la creazione del documento vengono buttati via. Eppure la memoria (working set) continua a crescere ad ogni giro fino a dare una OutOfMemoryException.
Il primo pensiero va giustamente ad un classico memory leak e qui parte un intreccio di code review e misurazioni sull’uso della memoria eseguite con il profiler di Visual Studio. Quando si incontra un managed memory leak i maggiori sospetti sono tipicamente per:
- Collection/hashtable/oggetti statici che accumulano un grosso numero di children senza rimuoverli
- Oggetti singleton che fungono da “root” per interi tree di oggetti
- Allocazioni native non rilasciate
Eppure non era nulla di tutto questo. Inoltre la traccia del profiler è decisamente poco generosa di informazioni utili. Si vede un esagerato numero di oggetti WPF referenziati internamente nelle classi di WPF e nulla di riconducibile al codice utente.
Il secondo tentativo è quello di forzare ad ogni ciclo una garbage collection con il classico pattern:
GC.Collect();
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Disclaimer: questo è solo codice per eseguire dei test e non dovrebbe mai essere usato in produzione.
I primi due Collect sono necessary per garantire che siano soggetti alla collection anche gli oggetti che hanno un finalizzatore. La WaitForPendingFinalizers serve ad attendere che la coda di finalizzazione sia vuota. L’ultima Collect garantisce che anche gli oggetti ormai finalizzati siano soggetti a collection.
Anche questo test non dà alcun esito, cioè la memoria non scende. A questo punto è sostanzialmente certa la presenza di un memory leak, cioè la presenza di un reference “da qualche parte” che non permetta al garbage collector di liberare la memoria.
Limitando il numero di cicli e forzando il garbage collector dopo un paio di secondi (tramite un pulsante) la memoria veniva liberata. Ma che razza di memory leak è questo?
E arriviamo al terzo tentativo. Mi dico: “come fa WPF a gestire oggetti grafici (i.e. risorse unmanaged di directx) senza aver mai bisogno di Dispose?”. Mi rispondo: “dovessi farlo io, implementerei un gestore di oggetti che li libera non appena non vengono più usati”.
Visto che l’apartment dei thread di WPF deve essere di tipo STA (Single Threading Apartment), se non permetto al dispatcher (che suppongo sia basato su una message pump di un oggetto COM di tipo STA) di svuotare la coda dei suoi messaggi, questo non potrà eseguire il metodo che presuppongo sia preposto al rilascio delle risorse e a ruota dei reference agli oggetti.
Per verificare la mia ipotesi, ad ogni ciclo faccio eseguire:
1: Application.Current.Dispatcher.Invoke(DispatcherPriority.Background, new ThreadStart(delegate { }));
La priorità di tipo Background è fondamentale perché, come dice MSDN, il delegate vuoto verrà eseguito ... “Operations are processed after all other non-idle operations are completed”. In pratica il codice del delegate “{}” è una scusa per assicurarsi che il dispatcher svuoti prima la coda dei messaggi.
Bingo, funziona! Adesso ad ogni ciclo la memoria viene liberata senza alcuna necessità di invocare manualmente la collection.
Considerazioni finali.
1. Le motivazioni appena descritte sono solo una mia deduzione. Non ho trovato alcuna documentazione in merito sul funzionamento interno di WPF per quello che riguarda la disallocazione di oggetti e risorse.
2. Anche eseguendo il ciclo in un thread separato il problema si verifica comunque.
Il thread secondario deve essere aperto con apartment STA e come tale necessita comunque di alimentare il dispatcher in quel thread.
3. Se è effettivamente come ho descritto, si spiega il fatto che le chiamate alla GC.Collect non abbiano effetto.
Quello che posso dedurre è che WPF si comporti come un runtime che, in modo analogo a quanto viene fatto dal garbage collector, si accorge quando un reference non viene più usato. A quel punto, alla passata successiva, il dispatcher può rilasciare le risorse unmanaged sottostanti e finalmente rilasciare l’oggetto al GC che eseguirà il rilascio reale della memoria occupata.
Perciò fin tanto che WPF non ha processato questi oggetti, anche se il codice utente non ha più reference attivi, è WPF ad averli perché questi oggetti devono ancora essere soggetti al rilascio di risorse interno di WPF.
Ripeto: sono solo mie deduzioni e non ho alcuna conferma a sostegno del mio ragionamento, se non i test che ho eseguito.
Morale.
Il garbage collector funziona, e funziona molto bene.
WPF fa il suo dovere e se il funzionamento è quello che ho intuito osservando questo test, i vantaggi sono superiori al problema che ho incontrato. L’assenza di Dispose in tutto WPF è un vantaggio non da poco e da qualche parte le risorse unmanaged ci devono essere per forza visto che si tratta di oggetti grafici.
Perciò come al solito prima di pensare a malfunzionamenti e bug bisogna cercare di capire come funziona e mettersi nei panni di chi ha dovuto risolvere il problema del rilascio delle risorse.