posts - 644, comments - 2003, trackbacks - 137

My Links

News

Raffaele Rialdi website

Su questo sito si trovano i miei articoli, esempi, snippet, tools, etc.

Archives

Post Categories

Image Galleries

Blogs

Links

WPF, elaborazioni lunghe e presunti memory leak

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:

  1. 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
  2. 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.

Print | posted on sabato 21 febbraio 2009 00:15 |

Feedback

Gravatar

# re: WPF, elaborazioni lunghe e presunti memory leak

@Dario. Ciao e grazie a te per il commento. Io credo innanzitutto che migrare una app Winform in WPF senza ripensarla sia un grave errore. Detto questo, si tratti di migrazione o meno il problema può saltare fuori in modo viscido e molto pericoloso, ed è per questo che l'ho subito bloggato anche senza avere documentazione più solida sotto mano.
Adesso che WPF sta maturando spero che si cominci ad avere un po' più di informazioni sugli "inner details" così da poter capire meglio questi preziosi dettagli.
Più che open source vorrei tanto open documentation! E ripeto che i sorgenti hanno valore z e r o in confronto ad una buona documentazione.

@Nicolò. Grazie ma nessun mito, mi infilo in questi garbugli solo per tenere allenati i neuroni ;-)
23/02/2009 02:58 | Raffaele Rialdi
Gravatar

# re: WPF, elaborazioni lunghe e presunti memory leak

Complimenti Raffaele, ho aggiunto il post tra i preferiti, per quando mi si presenterà il problema.
adesso devo rileggerlo ancora una volta per capire tutto pienamento

ciao marco
23/02/2009 12:52 | Nostromo
Gravatar

# re: WPF, elaborazioni lunghe e presunti memory leak

Ciao Luciano,
l'argomento di ThreadStart è un anonymous delegate. In pratica gli passi un delegate ad una funzione vuota. È un 'trucco' per far si che il thread del dispatcher sia servito.
Per vb.net puoi definire un metodo vuoto:
new ThreadStart(AddressOf Fake))
Dove Fake è ovviamente una sub vuota:
Sub Fake()
End Sub
06/07/2009 21:01 | Raffaele Rialdi
Gravatar

# re: WPF, elaborazioni lunghe e presunti memory leak

@NinjaCross. Se così fosse, fammelo sapere :)
23/02/2010 03:10 | Raffaele Rialdi
Gravatar

# re: WPF, elaborazioni lunghe e presunti memory leak

@Federico <cit>"Usa il debugger, luke!"</cit> :) Quando VS.net si ferma a causa dell'eccezione (o se preferisci con un breakpoint che piazzi tu) guarda qual'è la variabile che vale Nothing e avrai la risposta.
Se, come immagino, il Application.Current.Dispatcher è null, significa che WPF non ha ancora creato il dispatcher. Probabilmente hai messo il codice in un posto che viene invocato troppo presto. Se non è questo, bisogna vedere chi è a null.
08/04/2010 15:38 | Raffaele Rialdi
Gravatar

# re: WPF, elaborazioni lunghe e presunti memory leak

E come per magia, oplà un bel "Il programma ha smesso di funzionare...." bye bye.

Analizzando la parte "umana" nel registro eventi il colpevole è sempre presentationcore e l'eccezione è un ble OutOfMemoryException. La dimensione Committ era ulteriormente salita e molto probabilmente Windows ha risposto picche all'affamato.

Forse devo cominciare a meditare di passare alle winforms o cambiare mestiere.
12/04/2010 11:50 | Federico
Gravatar

# re: WPF, elaborazioni lunghe e presunti memory leak

Federico, quello che ho scritto nel mio post è applicabile *solo* se fai elaborazioni con WPF senza avere finestre attive. Se hai una UI "regolare" e questa è responisva, allora il suggerimento di questo post non può essere la soluzione.
Detto ciò, devi armarti di profiler (se hai la versione di VS.net con profiler usa quello, oppure Ants di RedGate, o altri ancora che trovi sulla rete come CLR profiler e il profiler di WPF) e guarda dov'è finita la memoria.

Se hai fatto errori di managed leak, non ne scamperai neppure migrando tutto a winform (che è molto più prono ad errori di tipo leak).
12/04/2010 14:55 | Raffaele Rialdi
Gravatar

# re: WPF, elaborazioni lunghe e presunti memory leak

Federico, se c'è una finestra, allora è una UI regolare in quanto il dispatcher viene "servito" regolarmente.
Il problema che ho esposto in questo post è relativo ad elaborazioni in cui esegui, ad esempio, un loop dove chiami delle funzioni WPF per eseguire conversioni o elaborazioni di vario tipo. In questo caso il dispatcher non viene servito e, come ho scritto, deduco che non abbia il tempo di liberare la memoria.

Nel tuo caso l'importante è che il tuo codice sia inserito nei punti previsti da WPF e che non esegui loop infiniti dentro for/while/etc. Deve essere WPF che ti chiama nei vari eventi / override / etc.

Ti consiglio di postare il tuo problema sul newsgroup microsoft.public.it.dotnet.framework che seguo normalmente.
Ciao
12/04/2010 19:19 | Raffaele Rialdi
Gravatar

# re: WPF, elaborazioni lunghe e presunti memory leak

Ciao John28,
Se hai una applicazione visuale la logica di quel loop è sbagliata.
Tutte le UI a finestre funzionano ad "eventing" perciò il loop deve essere gestito dall'infrastruttura.
Eseguire un loop infinito come fa tu è sbagliato anche perché di fatto avrai sempre un core della CPU al 100% con un enorme abbattimento di performance (oltre che di consumo energetico che probabilmente non ti interessa ma c'è).

Invece di una sleep(50) devi usare un dispatcher timer che viene gestito dal sistema e che ti chiamerà nell'intervallo prestabilito. Tale valore avrà una piccola tolleranza, ma tieni conto che anche il valore della sleep non è "esattamente" ma "maggiore uguale a".
Dentro l'evento del timer potrai andare ad aggiornare i controlli perché sei già dentro il thread giusto.
Ovviamente il grosso del lavoro di polling dal PLC va fatto un thread diverso che dovrà condividere una queue con il thread principale (devi usare la queue thread safe del framework).

Il topic del post riguarda specificamente elaborazioni prive di finestra dove l'eventing non è possibile e in quel caso la chiamata ad invoke del post mi ha sempre dato i risultati desiderati.

Difficile dire di più un commento di un post ;-)
Buon lavoro
28/11/2012 14:23 | Raffaele Rialdi
Gravatar

# re: WPF, elaborazioni lunghe e presunti memory leak

Non so su quale dove chiami Elabora_schermi() ma se gira nel thread principale, ovviamente stai bloccando il dispatcher di wpf e questo non va bene.
Nel mio precedente messaggio avevo specificato che l'acquisizione deve avvenire su un thread separato che gestisce una coda in sharing con il thread principale.

Ad ogni modo, questo è un blog non un'assistenza.
Se hai domande da discutere è meglio farle in un forum di discussioni che viene certamente letto da più persone.
Grazie per la comprensione
28/11/2012 20:30 | Raffaele Rialdi
Comments have been closed on this topic.

Powered by:
Powered By Subtext Powered By ASP.NET