性能问题-取消订阅事件



在我的应用程序中,我注意到我处理事件的方式会导致性能问题。

我想知道这是否是意料之中的事,也许我做错了什么。有办法解决我的问题吗?

namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
var x = new Main();
x.Init();
Console.ReadLine();
}
}
public class Main
{
private Bar _bar;
private List<Foo> _foos;
public Main()
{
_bar = new Bar();
}
public void Init()
{
var sw = new Stopwatch();
sw.Restart();
_foos = new List<Foo>();
for (int i = 0; i < 10000; i++)
{
var newFoo = new Foo();
newFoo.Bar = _bar;
_foos.Add(newFoo);
}
sw.Stop();
Console.WriteLine("Init 10.000 Foos WITH un-subscribe event: {0} ms", sw.ElapsedMilliseconds);
_foos.Clear();
sw.Restart();
_foos = new List<Foo>();
for (int i = 0; i < 10000; i++)
{
var newFoo = new Foo();
newFoo.BarWithout = _bar;
_foos.Add(newFoo);
}
sw.Stop();
Console.WriteLine("Init 10.000 Foos WITHOUT un-subscribe event: {0} ms", sw.ElapsedMilliseconds);
_foos.Clear();
}
}
public class Bar
{
public event EventHandler<string> Stuff;
protected virtual void OnStuff(string e)
{
var stuff = this.Stuff;
if (stuff != null)
stuff(this, e);
}
}
public class Foo
{
private Bar _bar;
public Bar Bar
{
get { return _bar; }
set
{
if (_bar != null)
{
_bar.Stuff -= _bar_Stuff;
}
_bar = value;
if (_bar != null)
{
_bar.Stuff -= _bar_Stuff;
_bar.Stuff += _bar_Stuff;
}
}
}
public Bar BarWithout
{
get { return _bar; }
set
{
if (_bar != null)
{
//_bar.Stuff -= _bar_Stuff;    
}
_bar = value;
if (_bar != null)
{
//_bar.Stuff -= _bar_Stuff;
_bar.Stuff += _bar_Stuff;
}
}
}
private void _bar_Stuff(object sender, string e)
{
}
}
}

在这个示例代码中,我的Foo类有两个属性BarBarWithoutBarWithout属性对取消订阅的out进行了注释。

Main类中,Init方法我创建了两次1.000Foo对象,第一个例程设置Bar属性,第二个例程设置了BarWithout属性。在我的机器上,第一个程序大约需要2200毫秒,第二个程序大约大约需要5毫秒。

由于差距有点大,我想知道是否有更有效的方法来删除事件处理程序?

顺便说一句,是的,我知道我可以更改代码,让Main订阅Event of Bar,然后为列表中的所有Foo对象调用一个方法,有点希望有一些"更容易"的东西,而不需要重构当前的情况。

编辑:

对于4倍的数据(因此40.000而不是10.000),第一个例程已经花费了约28.000毫秒,而不是约20毫秒,因此第一个例程的速度慢了10多倍,数据只多了4倍。第二个例程保持不变,性能数据增加4倍=速度降低4倍。

让我们看看您在循环中实际在做什么:

var newFoo = new Foo();
newFoo.Bar = _bar;

因此,您每次都创建一个新的Foo,并为其分配一个(现有的)bar——这将导致Foo附加一个事件处理程序。

在任何情况下,都不存在已经分配了BarFoo。因此,在"旧的"Bar对象上永远不会发生事件处理程序的注销,因为没有旧的Bar对象。因此,setter开始时的以下条件永远不会为真,代码也不会运行:

if (_bar != null)
{
_bar.Stuff -= _bar_Stuff;
}

在每次迭代中,_bar都是null,所以注释掉这一行并没有任何区别。

这使得BarBarWithout之间的唯一区别在于以下部分:

if (_bar != null)
{
_bar.Stuff -= _bar_Stuff;
_bar.Stuff += _bar_Stuff;
}

这总是运行的,因为我们总是给它分配一个非null的Bar。事件附加也总是运行,所以不会有什么不同。这只剩下注销。在这一点上,我问你:你希望这会做什么?为什么要注销之后直接注册的同一事件处理程序?

您是否尝试从其他Foo中注销事件处理程序?这行不通;_bar_Stuff特定于您所在的当前实例,因此它不能是另一个Foo的处理程序。

因此,由于_bar_Stuff始终是Foo实例的事件处理程序,并且始终存在一个新的Foo,这意味着Bar届时将永远不会注册该事件处理程序。因此,该行试图删除从未注册过的事件处理程序。正如你的基准测试所示,这似乎很昂贵,所以你应该避免它

请注意,您的基准测试还有另一个问题,即_foos.Clear()。虽然这将清除列表并删除对foo的引用,但一个Bar实例仍然注册了这些事件处理程序。这意味着Bar保留对每个Foo对象的引用,防止它们被垃圾收集。此外,运行循环的频率越高,注册的事件处理程序就越多,因此取消订阅未从Bar订阅的事件处理器将花费更多的时间(如果首先运行BarWithOut基准测试,您可以很容易地看到这一点)。

所以tl;所有这些的dr是,你应该确保Foo正确地取消订阅该事件。

由于差距有点大,我想知道是否有更有效的方法来删除事件处理程序?

一般情况下-不。在这种特殊情况下-是。只需删除该代码。这是多余的。

想一想。属性setter应该是您取消订阅上一个事件源并订阅新事件源的唯一位置。因此,绝对没有必要取消订阅新的处理程序(因为你知道你的对象不应该订阅),你正在做的也没有操作。但当然,-=操作不知道这一点,必须浏览整个处理程序列表,才能发现没有什么可删除的。这是每个线性搜索算法的最坏情况,当在循环中使用时,会导致O(N^2)的时间复杂性,从而导致性能差异。

正确的实现应该类似于

public Bar Bar
{
get { return _bar; }
set
{
if (ReferenceEquals(_bar, value)) return; // Nothing to do
if (_bar != null) _bar.Stuff -= _bar_Stuff;
_bar = value;
if (_bar != null) _bar.Stuff += _bar_Stuff;
}
} 

您可以使用类似的HashSet来支持事件

private readonly HashSet<EventHandler> _eventHandlers = new HashSet<EventHandler>();
public event EventHandler MyEvent
{
add => _eventHandlers.Add(value);
remove => _eventHandlers.Remove(value);
}
protected virtual void OnMyEvent()
{
foreach (EventHandler eventHandler in _eventHandlers.ToList())
{
eventHandler.Invoke(this, EventArgs.Empty);
}
}

这只会大大提高许多事件订阅的性能。并且不可能使用同一事件处理程序进行多个订阅。您可以使用Dictionary<EventHandler, List<EventHandler>>来实现这一点。

相关内容

  • 没有找到相关文章

最新更新