在我的应用程序中,我注意到我处理事件的方式会导致性能问题。
我想知道这是否是意料之中的事,也许我做错了什么。有办法解决我的问题吗?
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
类有两个属性Bar
和BarWithout
。BarWithout
属性对取消订阅的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
附加一个事件处理程序。
在任何情况下,都不存在已经分配了Bar
的Foo
。因此,在"旧的"Bar
对象上永远不会发生事件处理程序的注销,因为没有旧的Bar
对象。因此,setter开始时的以下条件永远不会为真,代码也不会运行:
if (_bar != null)
{
_bar.Stuff -= _bar_Stuff;
}
在每次迭代中,_bar
都是null
,所以注释掉这一行并没有任何区别。
这使得Bar
和BarWithout
之间的唯一区别在于以下部分:
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>>
来实现这一点。