不调用Delegate.EndInvoke可能导致内存泄漏...真的吗?

14

在这个问题上已有很多讨论,每个人似乎都同意您应该始终调用Delegate.EndInvoke以防止内存泄漏(即使Jon Skeet也是这么说的!)。

我一直遵循这个指导方针而没有质疑过,但最近我实现了自己的AsyncResult类,并发现唯一可能泄漏的资源是AsyncWaitHandle。

(实际上它并不会真正泄漏,因为WaitHandle使用的本机资源封装在一个具有Finalizer的SafeHandle中,它会对垃圾收集器的finalize队列产生压力。即便如此,一个良好的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-30MG之间波动,很奇怪吧?(可能是因为当有AsyncCallback时执行时间较长,因此线程池中会有更多排队的委托)

你认为... "神话被打破"了吗?

P.S. ThreadPool.QueueUserWorkItem比Delegate.BeginInvoke效率高一百倍,最好用于fire & forget调用。


2
好问题!+1。顺便说一下,你看了太多《神奇实验室》了 ;) - RCIX
我同意,ThreadPool.QueueUserWorkItem 是一个非常出色但鲜为人知的方法。 - Anton
4个回答

11

您不能仅仅依赖当前的内存泄漏情况。框架团队未来可能会以某种方式更改代码,导致泄漏,而且由于官方政策是“必须调用EndInvoke”,因此这是“按设计要求操作”。

您真的想冒险选择依赖观察到的行为而不是文档要求吗?这样做可能会导致您的应用程序在将来某个时候突然发生内存泄漏。


2
此外,在未来版本的框架中猜测危险是没有必要的。正如我在答案中指出的那样,除了内存泄漏的可能性之外,不调用Delegate.EndInvoke将导致异常被忽略,并且无法知道操作是否成功完成。+1。 - Nick Guerrera

6

我进行了一项小测试,调用了一个Action委托并在其中抛出了异常。然后,我确保不会通过仅维护指定数量的线程同时运行并在删除调用结束时持续填充线程池来淹没线程池。以下是代码:

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分钟,经过3千万个BeginInvoke调用后,私有字节内存消耗保持不变(23 MB),句柄计数也是如此。似乎不存在泄漏问题。我曾阅读Jeffry Richter的《C# via CLR》一书,其中提到存在内存泄漏问题。至少在.NET 3.5 SP1中已不再是事实。

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

祝好, Alois Kraus


2
在某些情况下,BeginInvoke不需要EndInvoke(特别是在WinForms窗口消息中)。但是,在异步通信的BeginRead和EndRead等情况下,这确实很重要。如果您想执行fire-and-forget BeginWrite,那么您可能会在一段时间后陷入严重的内存问题中。
因此,一个测试并不能得出确定性结论。您需要处理许多不同类型的异步事件委托来正确处理您的问题。

你可能是对的,有些异步操作确实需要调用EndInvoke,但我说的是Delegate.BeginInvoke。 - Jeff Cyr

1
考虑以下示例,在我的计算机上运行了几分钟,并在达到工作集3.5 GB之前我决定终止它。
Action a = delegate { throw new InvalidOperationException(); };
while (true)
    a.BeginInvoke(null, null);

注意:确保在没有调试器附加或禁用“抛出异常时中断”和“用户未处理的异常时中断”的情况下运行它。

编辑:正如Jeff所指出的那样,这里的内存问题不是泄漏,而只是通过比系统更快地排队工作来压倒系统的情况。实际上,可以通过将throw替换为任何适当的长操作来观察到相同的行为。如果我们在BeginInvoke调用之间留出足够的时间,内存使用就是有界的。

从技术上讲,这使得原始问题无法回答。然而,无论是否会导致泄漏,不调用Delegate.EndInvoke都是一个坏主意,因为它可能会导致异常被忽略。


1
有趣,但你试过这个吗?Action a = delegate { throw new InvalidOperationException(); }; while (true) a.BeginInvoke(ar => { try { a.EndInvoke(ar); } catch { } }, null);当调用EndInvoke时,内存消耗看起来相同,内存可能只会在队列中排队更多的工作项而无法执行它们时才会变高,当你抛出异常时。 - Jeff Cyr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接