匿名函数中的内存泄漏

时间:2015-10-01 00:54:00

标签: c# .net asynchronous closures resharper

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...)引起的。

enter image description here enter image description here

根据我的理解,不应该是泄漏,因为没有循环引用(据我所知),因此GC应该考虑泄漏。我还调用Dispose来及时释放线程(bgw)+套接字(Ping对象)。 (即使我没有让GC最终清理它们,但它还没赢?)

建议的评论更改

  • 在Disposing之前删除事件句柄
  • 处理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();
        }
    }
}

2 个答案:

答案 0 :(得分:2)

没有内存泄漏。您使用的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();
}

运行探查器,允许应用程序到达输出的位置&#34;全部完成&#34;,等待几秒钟并拍摄新快照。您将看到不再有任何内存泄漏。

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

答案 1 :(得分:1)

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

当一个对象实现dispose时,你需要调用它。如果你不调用它,很可能会出现内存泄漏。你应该使用using语句,并最终尝试处理对象Simarly你应该在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类。 (继承自WaitHandle。)

ManualResetEvent具有与之关联的非托管资源,这是.NET Framework库中实现IDisposable的大多数类的典型代表。

编辑:尝试使用此...

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();
        }
    }
}