Skip to content

Automatically dispatch CollectionChanged notification to the UI thread - #22160

Open
jjkh wants to merge 2 commits into
AvaloniaUI:mainfrom
jjkh:threadsafe-collectionchanged
Open

Automatically dispatch CollectionChanged notification to the UI thread#22160
jjkh wants to merge 2 commits into
AvaloniaUI:mainfrom
jjkh:threadsafe-collectionchanged

Conversation

@jjkh

@jjkh jjkh commented Sep 7, 2026

Copy link
Copy Markdown

What does the pull request do?

This PR makes two changes to address crashes found in local testing:

  1. Make WeakEvents.CollectionChanged thread-safe by posting to the UI thread
  2. Use a ConcurrentStack rather than Stack for WeakHashList's static PooledList pool
    • Without this change, if multiple threads were raising CollectionChanged events for different collections, they could both pop the same PooledList from the pool, causing the same PooledList to be used by two threads at once

What is the current behavior?

Given the following test program:

<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        x:Class="ThreadTest.MainWindow"
        xmlns:local="using:ThreadCheck"
        x:DataType="local:MainWindow">
  <UniformGrid Columns="3" Rows="2">
    <ListBox ItemsSource="{Binding LogLines1}" />
    <ListBox ItemsSource="{Binding LogLines2}" />
    <ListBox ItemsSource="{Binding LogLines3}" />
    <ListBox ItemsSource="{Binding LogLines4}" />
    <ListBox ItemsSource="{Binding LogLines5}" />
    <ListBox ItemsSource="{Binding LogLines6}" />
  </UniformGrid>
</Window>
using Avalonia.Controls;
using System;
using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;

namespace ThreadTest;
public partial class MainWindow : Window
{
    public ObservableCollection<string> LogLines1 { get; } = [];
    public ObservableCollection<string> LogLines2 { get; } = [];
    public ObservableCollection<string> LogLines3 { get; } = [];
    public ObservableCollection<string> LogLines4 { get; } = [];
    public ObservableCollection<string> LogLines5 { get; } = [];
    public ObservableCollection<string> LogLines6 { get; } = [];

    public MainWindow()
    {
        InitializeComponent();
        DataContext = this;

        Random rand = new();
        ObservableCollection<string>[] collections = [LogLines1, LogLines2, LogLines3, LogLines4, LogLines5, LogLines6];
        foreach (var collection in collections)
        {
            Task.Run(() => {
                while (true) {
                    collection.Add(rand.GetHexString(32));
                    Thread.Sleep(rand.Next(10));
                }
            });
        }
    }
}

We get the following exceptions:

 	[System.NullReferenceException thrown]
>	Avalonia.Base.dll!Avalonia.Utilities.WeakEvent<System.Collections.Specialized.INotifyCollectionChanged, System.Collections.Specialized.NotifyCollectionChangedEventArgs>.Subscription.OnEvent(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs eventArgs) Line 134	C#
 	Avalonia.Base.dll!Avalonia.Utilities.WeakEvents..cctor.AnonymousMethod__5(object _, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) Line 14	C#
 	System.ObjectModel.dll!System.Collections.ObjectModel.ObservableCollection<string>.OnCollectionChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)	Unknown
 	System.ObjectModel.dll!System.Collections.ObjectModel.ObservableCollection<string>.OnCollectionChanged(System.Collections.Specialized.NotifyCollectionChangedAction action, object item, int index)	Unknown
 	System.ObjectModel.dll!System.Collections.ObjectModel.ObservableCollection<string>.InsertItem(int index, string item)	Unknown
 	System.Private.CoreLib.dll!System.Collections.ObjectModel.Collection<string>.Add(string item)	Unknown
 	ThreadTest.dll!ThreadTest.MainWindow..ctor.AnonymousMethod__0() Line 28	C#
 	[System.NullReferenceException thrown]
>	Avalonia.Base.dll!Avalonia.Utilities.WeakHashList<Avalonia.Utilities.IWeakEventSubscriber<System.Collections.Specialized.NotifyCollectionChangedEventArgs>>.GetAlive(System.Func<Avalonia.Collections.Pooled.PooledList<Avalonia.Utilities.IWeakEventSubscriber<System.Collections.Specialized.NotifyCollectionChangedEventArgs>>> factory) Line 277	C#
 	Avalonia.Base.dll!Avalonia.Utilities.WeakEvent<System.Collections.Specialized.INotifyCollectionChanged, System.Collections.Specialized.NotifyCollectionChangedEventArgs>.Subscription.OnEvent(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs eventArgs) Line 124	C#
 	Avalonia.Base.dll!Avalonia.Utilities.WeakEvents..cctor.AnonymousMethod__5(object _, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) Line 14	C#
 	System.ObjectModel.dll!System.Collections.ObjectModel.ObservableCollection<string>.OnCollectionChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)	Unknown
 	System.ObjectModel.dll!System.Collections.ObjectModel.ObservableCollection<string>.OnCollectionChanged(System.Collections.Specialized.NotifyCollectionChangedAction action, object item, int index)	Unknown
 	System.ObjectModel.dll!System.Collections.ObjectModel.ObservableCollection<string>.InsertItem(int index, string item)	Unknown
 	System.Private.CoreLib.dll!System.Collections.ObjectModel.Collection<string>.Add(string item)	Unknown
 	ThreadTest.dll!ThreadTest.MainWindow..ctor.AnonymousMethod__0() Line 28	C#

What is the updated/expected behavior with this PR?

With the changes from this PR, I am no longer able to reproduce the exceptions above.

Automated Testing

Because these exceptions require a thread race to reproduce, I'm not sure how to reliably test them in CI.
I have omitted tests for now -- please let me know if they're required.

@jjkh

jjkh commented Sep 7, 2026

Copy link
Copy Markdown
Author

Oops, this is a breaking change with the public WeakEvents.CollectionChanged becoming WeakEvents.ThreadSafeCollectionChanged.

I'm not sure what's best to do here - I changed the name to match WeakEvents.ThreadSafePropertyChanged, but that's probably not necessary?
I can leave the event name the same, or close this and open an issue for the feature request (like #20729) if desired.

@cla-avalonia

cla-avalonia commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator
  • All contributors have signed the CLA.

@jjkh

jjkh commented Sep 7, 2026

Copy link
Copy Markdown
Author

@cla-avalonia agree

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants