Wpf: Creating multi-thread UI has a low probability to crash

Created on 29 Jan 2019  路  9Comments  路  Source: dotnet/wpf

  • .NET Core Version: 3.0.100-preview-009812
  • Windows version: Windows 10.0.18323 19H1
  • Does the bug reproduce also in WPF for .NET Framework 4.8?: Yes

Problem description:

Necessary conditions:

  1. Create multiple WPF UI threads

    • In fact, two are enough, one is the main UI thread with the App class we usually write; a background UI thread, for example, to display the UI thread that starts the splash screen.

    • If you use two threads, you need a lot of repetitive trials to reproduce; and by creating more threads you can greatly improve the probability of a single recurrence

  2. These UI threads all display WPF windows
  3. This issue will occur in both WPF on .NET Core 3 and WPF on .NET Framework 4.8.

phenomenon:

聽- An exception is thrown and the application crashes

Actual behavior:

```
Exception thrown: 'System.NullReferenceException' in WindowsBase.dll
Object reference not set to an instance of an object.

System.NullReferenceException: Object reference not set to an instance of an object.
at System.IO.Packaging.PackagePart.CleanUpRequestedStreamsList()
at System.IO.Packaging.PackagePart.GetStream(FileMode mode, FileAccess access)
at System.Windows.Application.LoadComponent(Object component, Uri resourceLocator)
at Walterlv.Bugs.MultiThreadedUI.SplashWindow.InitializeComponent() in C:\Users\lvyi\Desktop\Walterlv.Bugs.MultiThreadedUI\Walterlv.Bugs.MultiThreadedUI\SplashWindow.xaml:line 1
at Walterlv.Bugs.MultiThreadedUI.SplashWindow..ctor() in C:\Users\lvyi\Desktop\Walterlv.Bugs.MultiThreadedUI\Walterlv.Bugs.MultiThreadedUI\SplashWindow.xaml.cs:line 24
at Walterlv.Bugs.MultiThreadedUI.Program.<>c__DisplayClass1_0.b__0() in C:\Users\lvyi\Desktop\Walterlv.Bugs.MultiThreadedUI\Walterlv.Bugs.MultiThreadedUI\Program.cs:line 33


![The exception caught in Visual Studio 2019](https://user-images.githubusercontent.com/9959623/51883630-bd685300-23be-11e9-9c71-a0a8d8d929e3.png)

 **Expected behavior:**
Don't crash.

 **Minimal repro:**

1. Create a new WPF project (either .NET Core 3 or .NET Framework 4.8)
2. Keep the automatically generated `App` and `MainWindow` unchanged, we create a new window `SplashWindow`.
3. Create a new `Program` class containing the Main function and set `Program` as the startup object (instead of `App`) in the project properties.

![The project structure](https://user-images.githubusercontent.com/9959623/51883617-acb7dd00-23be-11e9-970d-d3581d5dab41.png)

All other files remain the same as the default code generated by Visual Studio, and the code of Program.cs is as follows:

```csharp
using System;
using System.Threading;
using System.Windows.Threading;

namespace Walterlv.Bugs.MultiThreadedUI
{
    public class Program
    {
        [STAThread]
        private static void Main(string[] args)
        {
            for (var i = 0; i < 50; i++)
            {
                RunSplashWindow(i);
            }

            var app = new App();
            app.InitializeComponent();
            app.Run();
        }

        private static void RunSplashWindow(int index)
        {
            var thread = new Thread(() =>
            {
                var window = new SplashWindow
                {
                    Title = $"SplashWindow {index.ToString().PadLeft(2, ' ')}",
                };
                window.Show();
                Dispatcher.Run();
            })
            {
                IsBackground = true,
            };
            thread.SetApartmentState(ApartmentState.STA);
            thread.Start();
        }
    }
}

Remarks: Even if you add this code just before the Splash Window creating, this exception still occurs.

SynchronizationContext.SetSynchronizationContext(
    new DispatcherSynchronizationContext(
        Dispatcher.CurrentDispatcher));
issue-type-bug

Most helpful comment

I also encountered such a problem. How did you solve it

The root cause is apparently a race condition within the automatically generated InitializeComponent() method.

So the way I've worked around this is to call InitializeComponent() within a lock on a static singleton. For example:

public partial class YourUserControl
{
    static readonly object Gate = new object();

    public YourUserControl()
    {
        lock (Gate)
            InitializeComponent();
    }
}

Note: you can add code-behind for ResourceDictionarys too. The code-behind gives you a place to add the lock statement if needed:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                    mc:Ignorable="d"
                    xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
                    x:Class="Your.ResourceDictionary.Class.Here">
...

I've used the above in production code to work around this issue. Perhaps one would be able to find a more elegant solution by studying the source code behind the offending framework call to System.IO.Packaging.PackagePart.CleanUpRequestedStreamsList(), but I don't like spending my time trying to work around decades-old Microsoft bugs so I did the above and moved on.

All 9 comments

The null object may be _requestedStreams and this is the code that set it to null:

The same issue can be seen here:

Because I can't find out why this bug happens, so I just post the issue itself.

Thanks for filing this issue @walterlv. This item and #298 seem very similar, is the reason you opened a separate issue because the crashes have different callstacks?

@stevenbrix The two issues have something differences:

  1. I can reproduce this one but cannot reproduce #298
  2. There're different callstacks.

If it's recommended to merge these two issues, I'll do that.

Thanks @walterlv. I just wanted to make sure that the repos and symptoms were indeed different. I think we should keep these two separate considering they will have separate fixes.

@stevenbrix I've posted the code to GitHub:

The Walterlv.Bugs.MultiThreadedUI is the .NET Framework version and the Walterlv.Bugs.MultiThreadedUI.Core is the .NET Core version.

We're also running into this behavior with our .NET Framework 4.7.2 app. In our case, we start up another thread to render reports on, and will sometimes get the NullReferenceException mentioned here, and other times get a IndexOutOfRangeException in PackagePart.CleanUpRequestedStreamsList(). It seems that the multiple threads wind up working on the same PackagePart.

I also encountered such a problem. How did you solve it

I also encountered such a problem. How did you solve it

The root cause is apparently a race condition within the automatically generated InitializeComponent() method.

So the way I've worked around this is to call InitializeComponent() within a lock on a static singleton. For example:

public partial class YourUserControl
{
    static readonly object Gate = new object();

    public YourUserControl()
    {
        lock (Gate)
            InitializeComponent();
    }
}

Note: you can add code-behind for ResourceDictionarys too. The code-behind gives you a place to add the lock statement if needed:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                    mc:Ignorable="d"
                    xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
                    x:Class="Your.ResourceDictionary.Class.Here">
...

I've used the above in production code to work around this issue. Perhaps one would be able to find a more elegant solution by studying the source code behind the offending framework call to System.IO.Packaging.PackagePart.CleanUpRequestedStreamsList(), but I don't like spending my time trying to work around decades-old Microsoft bugs so I did the above and moved on.

Was this page helpful?
0 / 5 - 0 ratings