简体   繁体   English

性能问题 - 取消订阅事件

[英]Performance Issue - unsubscribing events

In my application I noticed that my way of handling Events is causing performance issues.在我的应用程序中,我注意到我处理事件的方式导致了性能问题。

I would like to know if thats to be expected, maybe I am doing something wrong there.我想知道这是否可以预期,也许我在那里做错了什么。 Is there a way to fix my problem?有没有办法解决我的问题?

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)
        {

        }
    }
}

In this sample code, my Foo class has 2 properties Bar and BarWithout .在这个示例代码中,我的Foo类有 2 个属性BarBarWithout The BarWithout property has the un-subscribing out commented. BarWithout属性已注释取消订阅。

In the Main class the Init method I am creating 2 times 10.000 Foo objects and the first routine sets the Bar property the second sets the BarWithout property.Main类中, Init方法创建了 2 次10.000 Foo对象,第一个例程设置Bar属性,第二个例程设置BarWithout属性。 On my machine the first routine takes ~2200 milliseonds and the second routine takes ~5ms.在我的机器上,第一个例程需要约 2200 毫秒,第二个例程需要约 5 毫秒。

Since the gap is kinda huge, I am wondering if there is a more efficient way of removing an Event handler?由于差距有点大,我想知道是否有更有效的方法来删除事件处理程序?

Btw, yes I know I could change the code so that Main subscribes to the Event of Bar and than calls a method for all Foo objects in the list, kinda hope there is something "easier" without the need to refactor the current situation.顺便说一句,是的,我知道我可以更改代码,以便 Main 订阅 Bar 事件,然后为列表中的所有 Foo 对象调用一个方法,有点希望有一些“更容易”的东西,而无需重构当前情况。

Edit:编辑:

With 4 times the data (so 40.000 instead of 10.000) the first routine already takes ~28.000 milliseconds compared to ~20 milliseconds, so the first routine is more than 10 times slower with only 4 times more data.有了 4 倍的数据(所以是 40.000 而不是 10.000),第一个例程已经花费了约 28.000 毫秒,而与约 20 毫秒相比,因此第一个例程慢了 10 倍以上,数据仅增加了 4 倍。 The second routine stays constant with the Performance 4 times more data = 4 times slower.第二个例程保持不变,性能 4 倍多的数据 = 4 倍慢。

Let's take a look at what you're actually doing in your loop:让我们来看看您在循环中实际执行的操作:

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

So you create a new Foo everytime and assign an (existing) bar to it—which causes the Foo to attach an event handler.所以你每次都创建一个新的Foo并为其分配一个(现有的) bar - 这会导致Foo附加一个事件处理程序。

In any case, there is never a Foo which already has a Bar assigned.在任何情况下,永远不会有一个Foo已经分配了一个Bar So there never happens a deregistration of the event handler on the “old” Bar object, since there is no old Bar object.因此,“旧” Bar对象上的事件处理程序永远不会被注销,因为没有旧Bar对象。 So the following condition at the beginning of your setter is never true and the code doesn't run:因此,setter 开头的以下条件永远不会为真,并且代码不会运行:

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

In every iteration _bar is null , so commenting out that line does not make any difference.在每次迭代中_barnull ,因此注释掉该行没有任何区别。

That leaves the only difference between Bar and BarWithout the following part:这留下了BarBarWithout之间的唯一区别, BarWithout以下部分:

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

This always runs since we always assign a non-null Bar to it.这总是运行,因为我们总是为其分配一个非空的Bar The event attaching also always runs so that can't make the difference.事件附加也总是运行,所以不会有什么不同。 Which leaves only the unregistration.只留下注销。 And at that point I ask you: What do you expect that to do?那时我问你:你希望它做什么? Why do you unregister the same event handler you register directly afterwards?为什么要取消注册之后直接注册的相同事件处理程序?

Do you try this as an attempt to unregister event handlers from other Foo s?您是否尝试将其作为尝试从其他Foo取消注册事件处理程序? That won't work;那行不通; the _bar_Stuff is specific to the current instance you're in, so it cannot be another Foo 's handler. _bar_Stuff特定于您所在的当前实例,因此它不能是另一个Foo的处理程序。

So since _bar_Stuff is always the event handler of the Foo instance, and since there is always a new Foo that means that Bar will never have that event handler registered at that point.因此,由于_bar_Stuff始终是Foo实例的事件处理程序,并且始终有一个新的Foo这意味着Bar永远不会在该点注册该事件处理程序。 So the line attempts to remove an event handler that was never registered.因此该行尝试删除从未注册的事件处理程序。 And as your benchmark shows, that seems to be expensive, so you should avoid it.正如您的基准测试所示,这似乎很昂贵,因此您应该避免使用它。

Note that your benchmark also has another issue which is the _foos.Clear() .请注意,您的基准测试还有另一个问题,即_foos.Clear() While this will clear the list and remove the references to the foos, the one Bar instance still has those event handlers registered.虽然这将清除列表并删除对 foos 的引用,但一个Bar实例仍然注册了这些事件处理程序。 That means that the Bar keeps a reference to every Foo object, preventing them from being garbage collected.这意味着Bar保留对每个Foo对象的引用,防止它们被垃圾收集。 Further, the more often you run your loop, the more event handlers are registered, so unsubscribing an event handler that was not subscribed from the Bar will take even more time (you can easily see that if first run the BarWithOut benchmark).此外,您运行循环的频率越高,注册的事件处理程序就越多,因此取消订阅未从Bar订阅的事件处理程序将花费更多时间(如果首先运行BarWithOut基准测试,您可以很容易地看到这BarWithOut )。

So the tl;dr of all this is that you should make sure that the Foo s unsubscribe from the event properly.所以所有这一切的 tl;dr 是你应该确保Foo正确取消订阅事件。

Since the gap is kinda huge, I am wondering if there is a more efficient way of removing an Event handler?由于差距有点大,我想知道是否有更有效的方法来删除事件处理程序?

In general - no.一般来说 - 没有。 In this particular case - yes.在这种特殊情况下 - 是的。 Just remove that code.只需删除该代码。 It's redundant.这是多余的。

Just think a bit.稍微想一想。 The property setter should be the only place where you unsubscribe from the previous event source and subscribe to the new one.属性设置器应该是您取消订阅前一个事件源并订阅新事件源的唯一位置。 Thus, there is absolutely no need to unsubscribe from the new one (because you know your object should not subscribed) and what are you doing is no op.因此,绝对没有必要取消订阅新对象(因为您知道您的对象不应该订阅)并且您在做什么是没有操作的。 But of course the -= operation does not have that knowledge and have to go through the whole list of handlers just to find that there is nothing to remove.但是当然-=操作没有这些知识,并且必须遍历整个处理程序列表才能发现没有任何内容可以删除。 This is the worst case for every linear search algorithm and leads to O(N^2) time complexity when used in a loop, hence the difference in the performance.这是每个线性搜索算法的最坏情况,在循环中使用时会导致 O(N^2) 时间复杂度,因此性能存在差异。

The correct implementation should be something like this正确的实现应该是这样的

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;
     }
} 

You can back an event with a HashSet like this您可以像这样使用 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);
    }
}

This will only give a significant performance boost with many event subscriptions.这只会显着提升许多事件订阅的性能。 and it is not possible to have multiple subscriptions with the same event handler.并且不可能有多个订阅具有相同的事件处理程序。 You could implement that with Dictionary<EventHandler, List<EventHandler>> .你可以用Dictionary<EventHandler, List<EventHandler>>

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM