【问题标题】:.NET Events calling back once callee goes out of scope: safe practice?一旦被调用者超出范围,.NET 事件就会回调:安全做法?
【发布时间】:2012-03-03 01:00:20
【问题描述】:

(抱歉,如果以前出现过这种情况,我已经搜索但没有找到任何与我的搜索词相关的内容)

鉴于以下情况:

void Method1 {
    Foo _foo = new Foo();
    _foo.DataReady += ProcessData();
    _foo.StartProcessingData();
}

void ProcessData() { //do something }

StartProcessingData() 是一个长时间运行的方法,最终(异步)引发DataReady 事件。 (假设它进行了服务调用或其他操作)

现在,_foo 曾经是一个类级别的变量,而事件过去是在构造函数中连接起来的。

但是,内存分析强调了这将如何将 _foo 及其所有依赖项永远保留在内存中,因此我对上述内容进行了更改。

我的问题是:有没有 GC 会毁掉一切的情况? Method1 很快结束(当然在事件触发之前),这意味着_foo 不再存在。但是,这是否意味着(因为 _foo 保留其事件的引用)ProcessData() 永远不会触发?或者,事件的存在是否足以让 _foo 在方法结束后保持活动状态,足以确保 ProcessData 触发?还是没有定论?

[在测试中,它运行良好——ProcessData 总是被调用。即使让StartProcessingData 花费很长时间,并且在中途强制GC 收集(使用RedGate 的Memory Profiler)也没有删除它。但我想确定!]

澄清:StartProcessingData() 立即返回。 Foo 对象类似于:

class Foo
{
SomeSerice _service;
event EventHandler<EventArgs> DataReady;

Foo()
{
_service = new SomeService();
_service.ServiceCallCompleted += _service_ServiceCallCompleted;
}

void StartProcessingData()
{
_service.ServiceCallAsync();
}

void _service_ServiceCallCompleted
{
DataReady(null,e);
}

所以,抽象和模拟一个长期运行的异步服务,使用事件来表示重要的,呃,事件。


这是一个完整的工作示例(控制台应用)

class Program
        {
            static void Main(string[] args)
            {
                Class1 _class1 = new Class1();
                Console.WriteLine("Disposing of Class 1");
                _class1 = null;

                GC.Collect();

                System.Threading.Thread.Sleep(15000);
                Console.Read();

            }

        }

        internal class Class1
        {
            internal Class1()
            {
                Foo _foo = new Foo();
                _foo.DataReady += new EventHandler<EventArgs>(_foo_DataReady);

                _foo.StartProcessingData();
            }

            void _foo_DataReady(object sender, EventArgs e)
            {
                Console.WriteLine("Class 1 Processing Data");
            }
        }

        class Foo
        {
            internal event EventHandler<EventArgs> DataReady = delegate { };

            internal void StartProcessingData()
            {
                System.Threading.Timer _timer = new System.Threading.Timer(OnTimer);

                Console.WriteLine("Firing event in 10 secs");
                _timer.Change(10000, System.Threading.Timeout.Infinite);
            }

            private void OnTimer(object state)
            {
                DataReady(this, null);
            }
    }

如果你运行它,你会得到:

Firing event in 10 secs
Disposing of Class 1
Class 1 Processing Data

【问题讨论】:

  • StartProcessingData 是静态的是不是错字?或者你的意思是把 _foo.StartProcessingData()
  • 是的,这是一个错字。现在修复:谢谢:)
  • 我不确定我是否完全关注你。我假设StartProcessingData 实际上会调用处理程序。如果是这种情况,那么 Method1 在此调用完成之前不会完成,就像现在的代码一样。
  • 抱歉,我没有明确说明 StartProcessingData 会异步引发事件。我将编辑原件。

标签: c# .net garbage-collection


【解决方案1】:

假设StartProcessingData() 是完全同步的(即不涉及线程)。直到事件触发后才会返回,并且 ProcessData() 将从 within _foo.StartProcessingData() 调用。如果您想验证这一点,请在ProcessData() 中放置一个断点并查看调用堆栈。

因此,话虽如此,_foo 不会在事件触发并调用处理程序时超出范围,因为Method1() 尚未返回。

现在,如果涉及到线程,这意味着在另一个线程中执行的代码必须持有对_foo 的引用;否则,事件是不可能被触发的。因此,_foo 仍然不是垃圾回收的候选对象。因此,无论哪种情况,您都无需担心_foo 会被垃圾收集。

(编辑)

现在挂钩_serviceServiceCallCompleted 事件意味着_service 持有对_foo 的引用,从而防止它被垃圾回收。

【讨论】:

  • StartProcessingData() 完成后、事件引发之前以及_foo 不存在时会发生什么?我最初认为该事件不会被引发,因为它已经不存在了,但事实似乎并非如此。
  • 如果StartProcessingData()是同步的,你是误解了执行顺序; StartProcessingData() 直到事件被触发后才会返回。如果涉及线程,则另一个线程持有对_foo 的引用并防止它被垃圾收集。将代码发布到您的 Foo 对象可能会有所帮助。
  • StartProcessingData 将立即返回。我将为 Foo 对象发布一些伪代码。拿你们俩说的话来说:这就是我感到困惑的原因。 _foo 保持对回调的引用(而不是相反),所以没有什么可以阻止 _foo 被 GC 处理。一旦方法完成,它就会超出范围。但是,该事件仍然可以触发。
  • 如果 StartProcessingData() 立即返回,那么线程正在进行,对吗?如果是这样,那么另一个线程有一个对 _foo 的引用,它使 _foo 保持活动状态。
  • 在垃圾收集方面,我总是有点头晕。我原本以为什么 FMM 是正确的,但后来发现了以下 SO 问题并认为相反:stackoverflow.com/questions/298261/… 但是,我随后点击了子链接:stackoverflow.com/questions/185931/… 其中引用了diditwith.net/2007/03/23/… .... 其中,让我一直回到与 FMM 相同的原始假设
【解决方案2】:

对象Foo 可以订阅来自对象Bar 的事件,然后放弃对Bar 的所有引用,而在大多数情况下不会影响Bar 触发事件的能力,因为Bar可能会由于某个线程或外部对象而触发其事件,只有当该线程或外部对象引用它时才会发生这种情况。有两个原因可能很重要 Foo 保留对 Bar 的引用;您的情况可能都不适用。

  1. 如果 `Foo` 没有保留对 `Bar` 的任何引用,它可能无法让 `Bar` 知道它以后是否发现自己不需要 `Bar` 的服务。它可以尝试使用稍后某个事件回调的 `Sender` 参数来取消订阅,但这至少存在两个问题: (1) 有时一个对象会代表另一个对象引发事件,在这种情况下,`Sender` 属性可能不识别持有事件订阅的对象; (2) 事件可能永远不会被引发,但是对订阅者的引用可能会阻止订阅者有资格进行垃圾收集,直到发布者是。
  2. 有可能(尽管不太可能)系统中任何地方对 `Bar` 的唯一其他引用可能是 `WeakReference` 类型,因此如果 `Foo` 不保留对 `Bar` 的强引用,则系统可能会使所有这些弱引用无效,从而有效地导致 `Bar` 消失。

我个人认为,在对象被放弃之前取消所有订阅是个好主意。人们可能期望事件发布者会在订阅者被遗弃时超出范围,但如果事件保持连接状态,任何使发布者保持活动状态的东西都会无用地保持被放弃的订阅者活动。如果这些被放弃的订阅者中的任何一个也是事件发布者,那么他们被放弃的订阅者反过来可能会毫无用处地保持活动状态,等等。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-02-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多