【问题标题】:Memory leak in anonymous function匿名函数中的内存泄漏
【发布时间】:2015-12-28 22:25:19
【问题描述】:

TLDR;

不平凡的内存泄漏,可以在 Resharper 中很容易地看到。请参阅下面的最小示例。


我在以下程序中看到内存泄漏,但不知道原因。

程序异步向多个主机发送 ping,并确定至少一个主机是否正常。为此,重复调用运行这些异步操作的方法 (SendPing()),该方法在后台线程中运行它们(不是必须,但在实际应用程序中,SendPing() 将由主 UI 线程调用不应该被阻止)。

这个任务看起来很简单,但我认为泄漏的发生是由于我在 SendPing() 方法中创建 lambdas 的方式。该程序可以更改为不使用 lambda,但我更感兴趣的是了解导致泄漏的原因。

public class Program
{

    static string[] hosts = { "www.google.com", "www.facebook.com" };

    static void SendPing()
    {
        int numSucceeded = 0;
        ManualResetEvent alldone = new ManualResetEvent(false);

        ManualResetEvent[] handles = new ManualResetEvent[hosts.Length];
        for (int i = 0; i < hosts.Length; i++)
            handles[i] = new ManualResetEvent(false);

        BackgroundWorker worker = new BackgroundWorker();
        worker.DoWork += (sender, args) =>
        {
            numSucceeded = 0;
            Action<int, bool> onComplete = (hostIdx, succeeded) =>
            {
                if (succeeded) Interlocked.Increment(ref numSucceeded);
                handles[hostIdx].Set();
            };

            for (int i = 0; i < hosts.Length; i++)
                SendPing(i, onComplete);

            ManualResetEvent.WaitAll(handles);
        };

        worker.RunWorkerCompleted += (sender, args) =>
        {
            Console.WriteLine("Succeeded " + numSucceeded);
            BackgroundWorker bgw = sender as BackgroundWorker;
            alldone.Set();
        };

        worker.RunWorkerAsync();
        alldone.WaitOne();
        worker.Dispose();
    }

    static void SendPing(int hostIdx, Action<int, bool> onComplete)
    {
        Ping pingSender = new Ping();
        pingSender.PingCompleted += (sender, args) =>
        {
            bool succeeded = args.Error == null && !args.Cancelled && args.Reply != null && args.Reply.Status == IPStatus.Success;
            onComplete(hostIdx, succeeded);
            Ping p = sender as Ping;
            p.Dispose();
        };

        string data = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
        byte[] buffer = Encoding.ASCII.GetBytes(data);
        PingOptions options = new PingOptions(64, true);
        pingSender.SendAsync(hosts[hostIdx], 2000, buffer, options, hostIdx);
    }

    private static void Main(string[] args)
    {
        for (int i = 0; i < 1000; i++)
        {
            Console.WriteLine("Send ping " + i);
            SendPing();
        }
    }
}

Resharper 显示泄漏是由于未收集的闭包对象 (c__DisplayClass...)。

据我了解,不应该有泄漏,因为没有循环引用(据我所知),因此 GC 应该处理泄漏。我还调用Dispose 及时释放线程(bgw)+ 套接字(Ping 对象)。 (即使我没有 GC 最终也会清理它们,不是吗?)

来自 cmets 的建议更改

  • 在处理之前删除事件句柄
  • 处置ManualResetEvent

但泄漏仍然存在!

更改程序:

public class Program
{

    static string[] hosts = { "www.google.com", "www.facebook.com" };

    static void SendPing()
    {
        int numSucceeded = 0;
        ManualResetEvent alldone = new ManualResetEvent(false);

        BackgroundWorker worker = new BackgroundWorker();
        DoWorkEventHandler doWork = (sender, args) =>
        {
            ManualResetEvent[] handles = new ManualResetEvent[hosts.Length];
            for (int i = 0; i < hosts.Length; i++)
                handles[i] = new ManualResetEvent(false);

            numSucceeded = 0;
            Action<int, bool> onComplete = (hostIdx, succeeded) =>
            {
                if (succeeded) Interlocked.Increment(ref numSucceeded);
                handles[hostIdx].Set();
            };

            for (int i = 0; i < hosts.Length; i++)
                SendPing(i, onComplete);

            ManualResetEvent.WaitAll(handles);
            foreach (var handle in handles)
                handle.Close();

        };

        RunWorkerCompletedEventHandler completed = (sender, args) =>
        {
            Console.WriteLine("Succeeded " + numSucceeded);
            BackgroundWorker bgw = sender as BackgroundWorker;
            alldone.Set();
        };

        worker.DoWork += doWork;
        worker.RunWorkerCompleted += completed;

        worker.RunWorkerAsync();
        alldone.WaitOne();
        worker.DoWork -= doWork;
        worker.RunWorkerCompleted -= completed;
        worker.Dispose();
    }

    static void SendPing(int hostIdx, Action<int, bool> onComplete)
    {
        Ping pingSender = new Ping();
        PingCompletedEventHandler completed = null;
        completed = (sender, args) =>
        {
            bool succeeded = args.Error == null && !args.Cancelled && args.Reply != null && args.Reply.Status == IPStatus.Success;
            onComplete(hostIdx, succeeded);
            Ping p = sender as Ping;
            p.PingCompleted -= completed;
            p.Dispose();
        };

        pingSender.PingCompleted += completed;

        string data = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
        byte[] buffer = Encoding.ASCII.GetBytes(data);
        PingOptions options = new PingOptions(64, true);
        pingSender.SendAsync(hosts[hostIdx], 2000, buffer, options, hostIdx);
    }


    private static void Main(string[] args)
    {
        for (int i = 0; i < 1000; i++)
        {
            Console.WriteLine("Send ping " + i);
            SendPing();
        }
    }
}

【问题讨论】:

  • 使用 ping.SendPingAsyncTask.WhenAll 可以简化您的代码。
  • 谢谢(我不知道SendPingAsync),但我必须为这个应用程序使用.NET 3,所以不能有Tasks
  • worker.RunWorkerCompletedworker.DoWork 是您的内存泄漏。您应该在处理之前取消订阅这些事件,但是因为您将它们作为 lambda 表达式,所以它变得更加困难。 Dispose 告诉对象要清理,但是您仍然有对无法释放的 worker 对象的活动引用,并且 GC 不会收集它们。我个人认为 lambda 处理程序是 C# 规范团队的错误选择。 PingCompleted 有同样的问题。
  • @RonBeyer 但是为什么呢?当工作线程被收集时(一旦工作线程被释放),事件处理程序引用是否会被丢弃?
  • 不,GC 不会自动断开事件处理程序,这取决于您在处理对象之前执行此操作。

标签: c# .net asynchronous closures resharper


【解决方案1】:

没有内存泄漏。您使用的 dotMemory 分析快照,实际上,在一个快照的上下文中,编译器为完成的事件处理程序创建的自动生成的类仍将在内存中。像这样重写你的主应用程序:

private static void Main(string[] args)
{
    for (int i = 0; i < 200; i++)
    {
        Console.WriteLine("Send ping " + i);
        SendPing();
    }

    Console.WriteLine("All done");
    Console.ReadLine();
}

运行分析器,让应用程序到达输出“全部完成”的位置,等待几秒钟并拍摄新快照。您将看到不再有任何内存泄漏。

值得一提的是,编译器为 PingCompleted 事件处理程序生成的类(即c_DisplayClass6)会在方法static void SendPing(int hostIdx, Action&lt;int, bool&gt; onComplete) 退出后留在内存中。当pingSender.PingCompleted += (sender, args) =&gt;... 执行时,pingSender 实例将引用c_DisplayClass6。在调用pingSender.SendAsync 期间,框架将保留对pingSender 的引用,以处理异步方法的运行及其完成。当方法 SendPing 退出时,您通过调用 pingSender.SendAsync 启动的异步方法仍然运行。因为pingSender 会存活一段时间,因此c_DisplayClass6 也会存活一段时间。但是,在pingSender.SendAsync 操作完成后,框架将释放其对pingSender 的引用。此时pingSenderc_DisplayClass6 都成为可垃圾回收的,最终垃圾回收器将收集它们。如果您像我上面提到的那样拍摄最后一张快照,您可以看到这一点。在该快照中,dotMemory 将不再检测到泄漏。

【讨论】:

  • 感谢您的解释。但是,在 ReadLine() 上等待几分钟后,我仍然没有看到 dotMemory 中显示的“非托管内存”下降。也许我会把它留一夜,看看它们什么时候被收集!澄清一下,我没有在程序中显式调用 Dispose,但是当程序位于上面的 ReadLine() 语句时,根据您的解释,我假设剩余内存将由 GC 清理。
  • 我们现在正在谈论不同的事情。托管内存泄漏和本机内存的高使用率是不同的事情。我正在解决您最初的问题 - 关于内存泄漏,我正在解释您实际上并没有事件处理程序泄漏。为了了解系统将在调用 SendPing 的 Main 方法中使用多少本机内存。即使您基本上什么都不做,您仍然会在本机内存之上使用大量本机内存。
【解决方案2】:

ManualResetEvent 实现 Dispose()。您正在实例化许多 ManualResetEvents 并且从不调用 dispose。

当一个对象实现 dispose 你需要调用它。如果你不调用它,很可能会有内存泄漏。您应该使用 using 语句,并尝试最终处理对象类似地,您还应该在 Ping 周围使用 using 语句。

编辑:这可能很有用....

When should a ManualResetEvent be disposed?

编辑:如此处所述...

https://msdn.microsoft.com/en-us/library/498928w2(v=vs.110).aspx

当您创建包含非托管资源的对象时,您必须 当你在你的 应用程序。

编辑:如此处所述...

https://msdn.microsoft.com/en-us/library/system.threading.manualresetevent(v=vs.100).aspx

Dispose() 释放当前实例使用的所有资源 等待句柄类。 (继承自 WaitHandle。)

ManualResetEvent 具有与之关联的非托管资源,这对于实现 IDisposable 的 .NET Framework 库中的大多数类来说是相当典型的。

编辑:尝试使用这个...

public class Program
{
    static string[] hosts = { "www.google.com", "www.facebook.com" };

    static void SendPing()
    {
        int numSucceeded = 0;
        using (ManualResetEvent alldone = new ManualResetEvent(false))
        {
            BackgroundWorker worker = null;
            ManualResetEvent[] handles = null;
            try
            {
                worker = new BackgroundWorker();
                DoWorkEventHandler doWork = (sender, args) =>
                {
                    handles = new ManualResetEvent[hosts.Length];
                    for (int i = 0; i < hosts.Length; i++)
                        handles[i] = new ManualResetEvent(false);

                    numSucceeded = 0;
                    Action<int, bool> onComplete = (hostIdx, succeeded) =>
                    {
                        if (succeeded) Interlocked.Increment(ref numSucceeded);
                        handles[hostIdx].Set();
                    };

                    for (int i = 0; i < hosts.Length; i++)
                        SendPing(i, onComplete);

                    ManualResetEvent.WaitAll(handles);
                    foreach (var handle in handles)
                        handle.Close();

                };

                RunWorkerCompletedEventHandler completed = (sender, args) =>
                {
                    Console.WriteLine("Succeeded " + numSucceeded);
                    BackgroundWorker bgw = sender as BackgroundWorker;
                    alldone.Set();
                };

                worker.DoWork += doWork;
                worker.RunWorkerCompleted += completed;

                worker.RunWorkerAsync();
                alldone.WaitOne();
                worker.DoWork -= doWork;
                worker.RunWorkerCompleted -= completed;
            }
            finally
            {
                if (handles != null)
                {
                    foreach (var handle in handles)
                        handle.Dispose();
                }
                if (worker != null)
                    worker.Dispose();
            }
        }
    }

    static void SendPing(int hostIdx, Action<int, bool> onComplete)
    {
        using (Ping pingSender = new Ping())
        {
            PingCompletedEventHandler completed = null;
            completed = (sender, args) =>
            {
                bool succeeded = args.Error == null && !args.Cancelled && args.Reply != null && args.Reply.Status == IPStatus.Success;
                onComplete(hostIdx, succeeded);
                Ping p = sender as Ping;
                p.PingCompleted -= completed;
                p.Dispose();
            };

            pingSender.PingCompleted += completed;

            string data = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
            byte[] buffer = Encoding.ASCII.GetBytes(data);
            PingOptions options = new PingOptions(64, true);
            pingSender.SendAsync(hosts[hostIdx], 2000, buffer, options, hostIdx);
        }
    }


    private static void Main(string[] args)
    {
        for (int i = 0; i < 1000; i++)
        {
            Console.WriteLine("Send ping " + i);
            SendPing();
        }
    }
}

【讨论】:

  • 由于 lambda 事件处理程序,它仍然存在内存泄漏。处置/使用不会解决这个问题。
  • 即使不调用 Dispose GC 最终也会收集它们不是吗?我看到的是持续的内存增长......
  • 我已添加编辑以回应您的评论。此外,我会添加基本上优秀的 .NET 开发人员查找他们实例化的每个类的文档,如果该类实现 Dispose,他们确保调用 Dispose。不这样做只会导致问题
  • @ubi 基本上是的......一旦应用程序关闭,您可以等待数小时或数天,否则您的应用程序将泄漏内存
  • @ubi 如果您是 ASP.NET 开发人员,您可能会认为 GC 比实际情况更具侵略性。在 ASP.NET 中,每个请求都在它自己的线程中处理,在请求结束时线程被终止并且 GC 确实积极地收集与终止线程关联的对象,因为这些未处理的资源被快速收集。如果您的应用程序有无限长生命周期的线程,您会发现 GC 的行为非常不同,并且似乎会泄漏内存。
猜你喜欢
  • 1970-01-01
  • 2017-01-06
  • 1970-01-01
  • 1970-01-01
  • 2011-01-17
  • 2017-01-31
  • 1970-01-01
  • 2012-09-16
  • 2013-10-09
相关资源
最近更新 更多