繁体   English   中英

性能问题 - 取消订阅事件

[英]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类有 2 个属性BarBarWithout BarWithout属性已注释取消订阅。

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

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

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

编辑:

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

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

这留下了BarBarWithout之间的唯一区别, BarWithout以下部分:

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

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

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

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

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

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

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

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

稍微想一想。 属性设置器应该是您取消订阅前一个事件源并订阅新事件源的唯一位置。 因此,绝对没有必要取消订阅新对象(因为您知道您的对象不应该订阅)并且您在做什么是没有操作的。 但是当然-=操作没有这些知识,并且必须遍历整个处理程序列表才能发现没有任何内容可以删除。 这是每个线性搜索算法的最坏情况,在循环中使用时会导致 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>>

暂无
暂无

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

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