性能问题 - 取消订阅事件

Performance Issue - unsubscribing events

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

我想知道这是否符合预期,也许我在那里做错了什么。 有办法解决我的问题吗?

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 class 有 2 个属性 BarBarWithoutBarWithout 属性 评论取消订阅。

MainclassInit方法中我创建了2次10.000Foo对象和第一个例程设置 Bar 属性 第二个设置 BarWithout 属性。在我的机器上,第一个例程需要约 2200 毫秒,第二个例程需要约 5 毫秒。

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

顺便说一句,是的,我知道我可以更改代码,以便 Main 订阅 Bar 的事件,而不是为列表中的所有 Foo 对象调用一个方法,希望有些东西 "easier" 不需要重构现状。

编辑:

使用 4 倍的数据(因此是 40.000 而不是 10.000),第一个例程已经花费了约 28.000 毫秒,而不是约 20 毫秒,因此第一个例程慢了 10 倍以上,数据仅增加了 4 倍。第二个例程保持不变,性能增加 4 倍的数据 = 慢 4 倍。

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

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

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

无论如何,从来没有 Foo 已经分配了 Bar。因此,“旧”Bar 对象上的事件处理程序永远不会注销,因为没有旧 Bar 对象。因此 setter 开头的以下条件永远不会为真,代码也不会 运行:

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

在每次迭代中 _bar 都是 null,因此注释掉该行没有任何区别。

BarBarWithout 之间的唯一区别是以下部分:

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

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

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

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

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

所以 tl;dr 所有这一切是你应该确保 Foos 正确取消订阅事件。

Since the gap is kinda huge, I am wondering if there is a more efficient way of removing an Event handler?

总的来说 - 没有。在这种特殊情况下 - 是的。只需删除该代码即可。这是多余的。

稍微想想。 属性 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>>.

来实现