GC 是罪魁祸首。期间。
首先,强事件处理程序的默认实现并未针对许多侦听器进行优化。要获得良好的性能,请考虑显式实现事件处理程序。使用哈希集,我们可以改进内部使用平面列表的默认实现。
修复
对于您的活动,使用HashSet 实现add 和remove。请注意,此实现不是线程安全的。如果多个线程将使用该事件,您可能需要添加锁定机制。
class Publisher : INotifyPropertyChanged
{
private HashSet<PropertyChangedEventHandler> propertyChangedHandlers =
new HashSet<PropertyChangedEventHandler>();
public event PropertyChangedEventHandler PropertyChanged
{
add => propertyChangedHandlers.Add(value);
remove => propertyChangedHandlers.Remove(value);
}
public void Signal()
{
var args = new PropertyChangedEventArgs(null);
foreach (var handler in propertyChangedHandlers)
{
handler(this, new PropertyChangedEventArgs(null));
}
}
public void RemoveSubscribers(IEnumerable<Listener> listeners)
{
foreach (var listener in listeners)
{
listener.Subscribe(this);
}
}
}
这会表现得更好。但是为什么?当然,从哈希集中删除项目比遍历平面列表要快得多,但它并没有那么慢。极端的 CPU 时间实际上来自GC。取消订阅事件时,将重新分配处理程序列表。旧的列表很快就会被垃圾收集器收集起来。
分析
在您的情况下,您有 1,000,000 事件处理程序。因此,每次取消订阅事件处理程序时,都会取消引用 N 项目列表(其中 N 接近 1000000)并分配新的 N-1 处理程序列表。这样做 100 次,GC 将有大量数据要收集到内存中:
100 * 1000000 * 8 字节 = ~800 MB
我们可以使用GC.TryStartNoGCRegion API 轻松证明这一点。下面的示例尝试取消订阅 20 个事件处理程序,但由于此操作确实会分配超过请求的 128 MB,因此它将失败:
未处理的异常:System.InvalidOperationException:已分配
内存超出 NoGCRegion 模式的指定内存
System.GC.EndNoGCRegionWorker()
class Publisher : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
class Listener
{
public void Subscribe(Publisher publisher)
{
publisher.PropertyChanged += OnPropertyChanged;
}
public void Unsubscribe(Publisher publisher)
{
publisher.PropertyChanged -= OnPropertyChanged;
}
private static void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
Console.WriteLine($"OnPropertyChanged called for {nameof(Listener)} {sender}");
}
}
class Program
{
static void Main(string[] args)
{
var publisher = new Publisher();
var listeners = Enumerable.Range(0, 1000000)
.Select(p => new Listener())
.ToList();
foreach (var listener in listeners)
{
listener.Subscribe(publisher);
}
var watch = new System.Diagnostics.Stopwatch();
watch.Start();
const int toRemove = 20;
if (GC.TryStartNoGCRegion(128L * 1024L * 1024L, true))
{
try
{
for (int i = 0; i < toRemove; i++)
{
listeners[i].Unsubscribe(publisher);
}
}
finally
{
watch.Stop();
var time = watch.ElapsedMilliseconds;
Console.WriteLine($"Removing {toRemove} handlers: {time} ms");
GC.EndNoGCRegion();
}
}
}
}
如果我们运行内存分析器,我们可以看到 删除 事件处理程序确实会导致分配和GC。
更多详情请见MulticastDelegate.cs(360)。