代码之家  ›  专栏  ›  技术社区  ›  Jeff Cyr

不调用delegate.endinvoke会导致内存泄漏…一个神话?

  •  14
  • Jeff Cyr  · 技术社区  · 16 年前

    关于这一点已经讨论了很多,每个人都倾向于同意您应该始终调用delegate.endinvoke来防止内存泄漏(甚至是jon skeet也这么说!).

    我总是毫无疑问地遵循这个准则,但最近我实现了自己的AsyncResult类,并发现唯一可能泄漏的资源是AsyncWaitHandle。

    (实际上,它不会真正泄漏,因为waithandle使用的本机资源封装在具有终结器的safehandle中,但它会对垃圾收集器的终结队列增加压力。即使如此,AsyncResult的良好实现也只能根据需要初始化AsyncWaitHandle…)

    知道是否有泄漏的最好方法就是尝试一下:

    Action a = delegate { };
    while (true)
        a.BeginInvoke(null, null);
    

    我运行了一会儿,内存保持在9-20 MB之间。

    让我们比较一下何时调用delegate.endinvoke:

    Action a = delegate { };
    while (true)
        a.BeginInvoke(ar => a.EndInvoke(ar), null);
    

    通过这个测试,记忆在9-30毫克之间,很奇怪吧?(可能是因为当存在异步回调时,执行时间稍长,因此线程池中将有更多排队的委托)

    你觉得……”神话破灭了?

    p.s.threadpool.queueuserworkitem比delegate.begininvoke效率高一百倍,最好将其用于fire&forget调用。

    4 回复  |  直到 9 年前
        1
  •  6
  •   gevorg Dathan    9 年前

    我运行了一个小测试来调用一个动作委托并在其中抛出一个异常。然后,我要确保不会通过一次只运行指定数量的线程来释放线程池,并在删除调用结束时继续填充线程池。代码如下:

    static void Main(string[] args)
    {
    
        const int width = 2;
        int currentWidth = 0;
        int totalCalls = 0;
        Action acc = () =>
        {
            try
            {
                Interlocked.Increment(ref totalCalls);
                Interlocked.Increment(ref currentWidth);
                throw new InvalidCastException("test Exception");
            }
            finally
            {
                Interlocked.Decrement(ref currentWidth);
            }
        };
    
        while (true)
        {
            if (currentWidth < width)
            {
                for(int i=0;i<width;i++)
                    acc.BeginInvoke(null, null);
            }
    
            if (totalCalls % 1000 == 0)
                Console.WriteLine("called {0:N}", totalCalls);
    
        }
    }
    

    在让它运行大约20分钟和超过3000万次BeginInvoke调用之后,私有字节内存消耗和句柄计数是恒定的(23MB)。似乎没有泄漏。我读过杰弗里·里克特的书C通过clr,他说有内存泄漏。至少在.NET 3.5 SP1中似乎不再是这样。

    测试环境: Windows 7 x86 .NET 3.5 SP1 Intel 6600双核2.4 GHz

    你的, 阿洛伊斯克劳斯

        2
  •  11
  •   John Saunders    16 年前

    它当前是否泄漏内存不是您应该依赖的。框架团队将来可能会以一种可能导致泄漏的方式改变事情,因为官方政策是“你必须称之为endinvoke”,那么它就是“按设计”。

    你真的想冒险在将来某个时候你的应用会突然开始泄漏内存,因为你选择依赖观察到的行为而不是文档化的需求吗?

        3
  •  2
  •   John Fisher    16 年前

    在某些情况下,BeginInvoke不需要EndInvoke(尤其是在WinForms窗口消息传递中)。但是,在某些情况下,这很重要——比如异步通信的beginread和endread。如果你想生火而忘了贝金华,你可能会在一段时间后陷入严重的记忆障碍。

    所以,你的一个测试不可能是决定性的。您需要处理许多不同类型的异步事件委托才能正确处理您的问题。

        4
  •  1
  •   Nick Guerrera    16 年前

    考虑下面的例子,它在我的机器上运行了几分钟,在我决定杀死它之前达到了3.5GB的工作集。

    Action a = delegate { throw new InvalidOperationException(); };
    while (true)
        a.BeginInvoke(null, null);
    

    注意:确保在没有附加调试程序的情况下运行它,或者禁用了“引发异常时中断”和“用户未处理异常时中断”。

    编辑: 正如Jeff指出的那样,这里的内存问题不是泄漏,而是一个简单的情况,即通过排队来压倒系统的工作速度超过了它的处理速度。事实上,同样的行为可以通过用任何适当的长时间操作来代替投掷来观察。如果在BeginInvoke调用之间留出足够的时间,那么内存使用是有限制的。

    从技术上讲,这使得最初的问题没有答案。但是,不管它是否会导致泄漏,不要调用delegate.endinvoke是一个坏主意,因为它会导致忽略异常。