Browse Source

WeakEvent subscription management thread race condition fix (#19383)

* Fixed thread-race condition when INotifyCollectionChanged event fired on non-ui thread

* Fixed WeakEvent thread-race condition (NullReferenceException) when event fired on non-ui thread

* Limited delegates creation in WeakEvent

* Avoid casting in WeakEvent
release/11.3.4
Grigorii Vasilchenko 6 months ago
committed by Julien Lebosquain
parent
commit
5968918bed
  1. 111
      src/Avalonia.Base/Utilities/WeakEvent.cs
  2. 12
      src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs

111
src/Avalonia.Base/Utilities/WeakEvent.cs

@ -1,5 +1,7 @@
using System; using System;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading;
using Avalonia.Collections.Pooled;
using Avalonia.Threading; using Avalonia.Threading;
namespace Avalonia.Utilities; namespace Avalonia.Utilities;
@ -10,8 +12,8 @@ namespace Avalonia.Utilities;
public sealed class WeakEvent<TSender, TEventArgs> : WeakEvent where TSender : class public sealed class WeakEvent<TSender, TEventArgs> : WeakEvent where TSender : class
{ {
private readonly Func<TSender, EventHandler<TEventArgs>, Action> _subscribe; private readonly Func<TSender, EventHandler<TEventArgs>, Action> _subscribe;
private readonly ConditionalWeakTable<TSender, Subscription> _subscriptions = new();
private readonly ConditionalWeakTable<object, Subscription> _subscriptions = new(); private readonly ConditionalWeakTable<TSender, Subscription>.CreateValueCallback _createSubscription;
internal WeakEvent( internal WeakEvent(
Action<TSender, EventHandler<TEventArgs>> subscribe, Action<TSender, EventHandler<TEventArgs>> subscribe,
@ -22,33 +24,43 @@ public sealed class WeakEvent<TSender, TEventArgs> : WeakEvent where TSender : c
subscribe(t, s); subscribe(t, s);
return () => unsubscribe(t, s); return () => unsubscribe(t, s);
}; };
_createSubscription = CreateSubscription;
} }
internal WeakEvent(Func<TSender, EventHandler<TEventArgs>, Action> subscribe) internal WeakEvent(Func<TSender, EventHandler<TEventArgs>, Action> subscribe)
{ {
_subscribe = subscribe; _subscribe = subscribe;
_createSubscription = CreateSubscription;
} }
public void Subscribe(TSender target, IWeakEventSubscriber<TEventArgs> subscriber) public void Subscribe(TSender target, IWeakEventSubscriber<TEventArgs> subscriber)
{ {
if (!_subscriptions.TryGetValue(target, out var subscription)) var spinWait = default(SpinWait);
_subscriptions.Add(target, subscription = new Subscription(this, target)); while (true)
subscription.Add(subscriber); {
var subscription = _subscriptions.GetValue(target, _createSubscription);
if (subscription.Add(subscriber))
break;
spinWait.SpinOnce();
}
} }
public void Unsubscribe(TSender target, IWeakEventSubscriber<TEventArgs> subscriber) public void Unsubscribe(TSender target, IWeakEventSubscriber<TEventArgs> subscriber)
{ {
if (_subscriptions.TryGetValue(target, out var subscription)) if (_subscriptions.TryGetValue(target, out var subscription))
subscription.Remove(subscriber); subscription.Remove(subscriber);
} }
private Subscription CreateSubscription(TSender key) => new(this, key);
private sealed class Subscription private sealed class Subscription
{ {
private readonly WeakEvent<TSender, TEventArgs> _ev; private readonly WeakEvent<TSender, TEventArgs> _ev;
private readonly TSender _target; private readonly TSender _target;
private readonly Action _compact; private readonly Action _compact;
private readonly Action _unsubscribe;
private readonly WeakHashList<IWeakEventSubscriber<TEventArgs>> _list = new(); private readonly WeakHashList<IWeakEventSubscriber<TEventArgs>> _list = new();
private readonly object _lock = new();
private Action? _unsubscribe;
private bool _compactScheduled; private bool _compactScheduled;
private bool _destroyed; private bool _destroyed;
@ -57,7 +69,6 @@ public sealed class WeakEvent<TSender, TEventArgs> : WeakEvent where TSender : c
_ev = ev; _ev = ev;
_target = target; _target = target;
_compact = Compact; _compact = Compact;
_unsubscribe = ev._subscribe(target, OnEvent);
} }
private void Destroy() private void Destroy()
@ -65,19 +76,42 @@ public sealed class WeakEvent<TSender, TEventArgs> : WeakEvent where TSender : c
if(_destroyed) if(_destroyed)
return; return;
_destroyed = true; _destroyed = true;
_unsubscribe(); _unsubscribe?.Invoke();
_ev._subscriptions.Remove(_target); _ev._subscriptions.Remove(_target);
} }
public void Add(IWeakEventSubscriber<TEventArgs> s) => _list.Add(s); public bool Add(IWeakEventSubscriber<TEventArgs> s)
{
if (_destroyed)
return false;
lock (_lock)
{
if (_destroyed)
return false;
_unsubscribe ??= _ev._subscribe(_target, OnEvent);
_list.Add(s);
return true;
}
}
public void Remove(IWeakEventSubscriber<TEventArgs> s) public void Remove(IWeakEventSubscriber<TEventArgs> s)
{ {
_list.Remove(s); if (_destroyed)
if(_list.IsEmpty) return;
Destroy();
else if(_list.NeedCompact && _compactScheduled) lock (_lock)
ScheduleCompact(); {
if (_destroyed)
return;
_list.Remove(s);
if(_list.IsEmpty)
Destroy();
else if(_list.NeedCompact && _compactScheduled)
ScheduleCompact();
}
} }
private void ScheduleCompact() private void ScheduleCompact()
@ -90,23 +124,40 @@ public sealed class WeakEvent<TSender, TEventArgs> : WeakEvent where TSender : c
private void Compact() private void Compact()
{ {
if(!_compactScheduled) if (_destroyed)
return; return;
_compactScheduled = false;
_list.Compact(); lock (_lock)
if (_list.IsEmpty) {
Destroy(); if (_destroyed)
return;
if(!_compactScheduled)
return;
_compactScheduled = false;
_list.Compact();
if (_list.IsEmpty)
Destroy();
}
} }
private void OnEvent(object? sender, TEventArgs eventArgs) private void OnEvent(object? sender, TEventArgs eventArgs)
{ {
var alive = _list.GetAlive(); PooledList<IWeakEventSubscriber<TEventArgs>>? alive;
if(alive == null) lock (_lock)
Destroy(); {
else alive = _list.GetAlive();
if (alive == null)
{
Destroy();
return;
}
}
foreach(var item in alive.Span)
item.OnEvent(_target, _ev, eventArgs);
lock (_lock)
{ {
foreach(var item in alive.Span)
item.OnEvent(_target, _ev, eventArgs);
WeakHashList<IWeakEventSubscriber<TEventArgs>>.ReturnToSharedPool(alive); WeakHashList<IWeakEventSubscriber<TEventArgs>>.ReturnToSharedPool(alive);
if(_list.NeedCompact && !_compactScheduled) if(_list.NeedCompact && !_compactScheduled)
ScheduleCompact(); ScheduleCompact();
@ -124,13 +175,13 @@ public class WeakEvent
{ {
return new WeakEvent<TSender, TEventArgs>(subscribe, unsubscribe); return new WeakEvent<TSender, TEventArgs>(subscribe, unsubscribe);
} }
public static WeakEvent<TSender, TEventArgs> Register<TSender, TEventArgs>( public static WeakEvent<TSender, TEventArgs> Register<TSender, TEventArgs>(
Func<TSender, EventHandler<TEventArgs>, Action> subscribe) where TSender : class where TEventArgs : EventArgs Func<TSender, EventHandler<TEventArgs>, Action> subscribe) where TSender : class where TEventArgs : EventArgs
{ {
return new WeakEvent<TSender, TEventArgs>(subscribe); return new WeakEvent<TSender, TEventArgs>(subscribe);
} }
public static WeakEvent<TSender, EventArgs> Register<TSender>( public static WeakEvent<TSender, EventArgs> Register<TSender>(
Action<TSender, EventHandler> subscribe, Action<TSender, EventHandler> subscribe,
Action<TSender, EventHandler> unsubscribe) where TSender : class Action<TSender, EventHandler> unsubscribe) where TSender : class

12
src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs

@ -132,18 +132,22 @@ namespace Avalonia.Controls.Utils
} }
} }
var l = Listeners.ToArray();
if (Dispatcher.UIThread.CheckAccess()) if (Dispatcher.UIThread.CheckAccess())
{ {
var l = Listeners.ToArray();
Notify(_collection, e, l); Notify(_collection, e, l);
} }
else else
{ {
var eCapture = e; var eCapture = e;
Dispatcher.UIThread.Post(() => Notify(_collection, eCapture, l), DispatcherPriority.Send); Dispatcher.UIThread.Post(() =>
{
var l = Listeners.ToArray();
Notify(_collection, eCapture, l);
},
DispatcherPriority.Send);
} }
} }
} }
} }
} }
Loading…
Cancel
Save