WPF leaks memory when using separate STA Thread

By Mirek on (tags: Dispatcher, memory leak, STA, thread, WPF, categories: code)

Recently, I was asked to tackle down a memory leak problem in WPF application. The problem was so not obvious and so mysterious, that triggered bigger research on a topic.

The problem code was quite simple. We have a WPF application which opens new window on separate thread. The thread must be STA thread to fulfill the WPF requirement. That window then displays an image loaded from a file. This is how the App.xaml.cs code looks like

public partial class App : Application
{
     protected override void OnStartup(StartupEventArgs e)
     {
         RunInSTAThread(ShowImageWindow);
     }
     static void RunInSTAThread(Action actionToCall)
     {
         Thread thread = new Thread(new ThreadStart(actionToCall));
         thread.SetApartmentState(ApartmentState.STA);
         thread.Start();
     }
     static void ShowImageWindow()
     {
         var window = new Window();
         window.Content = CreateImageFromFile();
         window.Show();
         window.Close();
     }

and the CreateImageFromFile method

static Image CreateImageFromFile()
{
     var imageByte = File.ReadAllBytes("image.jpg");
     using var stream = new MemoryStream(imageByte);
     var bitmap = new BitmapImage();
     stream.Position = 0;
     bitmap.BeginInit();
     bitmap.CreateOptions = BitmapCreateOptions.PreservePixelFormat;
     bitmap.CacheOption = BitmapCacheOption.OnLoad;
     bitmap.UriSource = null;
     bitmap.StreamSource = stream;
     bitmap.EndInit();
     bitmap.Freeze();
     var image = new Image();
     image.Source = bitmap;
     return image; }

Of course the application has some empty window assigned as MainWindow by default so it does not terminates when we leave the OnStartup method.

Nothing fancy, right? But the problem is the window and all its children allocated in this separate thread never gets collected by GC and the memory remains occupied. When we inspect the memory snapshot in Visual Studio diagnostic tool, we can clearly see that the BitmapImage and its undergoing MediaContext with the image fully loaded, still stays on the heap. Even, though the bitmap has been frozen, which is recommended, good practice to avoid memory leaks and speed up performance, it is not being freed up after leaving the thread.

To understand the reason of this behavior we first need to understand the internals of the Windows Presentation Foundation and it’s connections to the STA thread.

There is great paper on the WPF topic by Shawn Wildermuth. Thus I’ll quote crucial parts of it as it clearly describes the idea behind :

All WPF applications start out with two important threads, one for rendering and one for managing the user interface. The rendering thread is a hidden thread that runs in the background, so the only thread that you ordinarily deal with is the UI thread. WPF requires that most of its objects be tied to the UI thread. This is known as thread affinity, meaning you can only use a WPF object on the thread on which it was created. Using it on other threads will cause a runtime exception to be thrown.

The thread affinity is handled by the Dispatcher class, a prioritized message loop for WPF applications. Typically your WPF projects have a single Dispatcher object (and therefore a single UI thread) that all user interface work is channeled through.

and later on we can read

The Dispatcher class provides a gateway to the message pump in WPF and provides a mechanism to route work for processing by the UI thread. This is necessary to meet the thread affinity demands, but since the UI thread is blocked for each piece of work routed through the Dispatcher, it is important to keep the work that the Dispatcher does small and quick.

So essentially whenever we are dealing with WPF visual elements, we are dealing with the Dispatcher object underneath. That is, all WPF visuals derive from DispatcherObject class which gives access to the underlying Dispatcher. Moreover dispatcher is in some way tied to the running thread.
Having said that, what should we then do in our example? Create new dispatcher and run it? Not necessarily, the Dispatcher is created on demand and moreover there cannot be more than one dispatcher per thread basically. So the WPF framework is controlling the creation and running the Dispatcher behind the scenes.

The only thing we could do is to shutdown the dispatcher, so it stops processing messages and release all resources including the running thread. An idea how to do it is well described in this blog post. Also in the official Microsoft documentation for Dispatcher class in the remarks section stays as follows:

If you attempt to get the CurrentDispatcher for the current thread and a Dispatcher is not associated with the thread, a Dispatcher will be created. A Dispatcher is also created when you create a DispatcherObject. If you create a Dispatcher on a background thread, be sure to shut down the dispatcher before exiting the thread.

Ok, but we never need to shutdown the dispatcher when we’re dealing with “normal” WPF application and the memory is somehow released, why do we need to do it in this case ? Well, “normal” WPF application starts with creating an object of type Application, which is an entry point for the entire program and handles dispatcher internally including shutting it down on application exit. Hovewer, in the example described in this post, we only create a Window object, not the Application, so we must handle it by ourselves.

So let’s see how can we fix it then:

static void ShowImageWindow()
{
     try
     {
         var window = new Window();
         window.Content = CreateImageFromFile();
         window.Show();
         window.Close();
     }
     finally
     {
         System.Windows.Threading.Dispatcher
             .CurrentDispatcher
             .InvokeShutdown();
     } }

We access the default dispatcher that is associated with current thread and tell it to start shutting down. That’s it!
We could  also put that code in the window’s Closed event handler in case the window is about to stay visible unless the user closes it directly.