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最终清理它们,但它还没赢?)
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();
}
}
}
答案 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
的引用。此时,pingSender
和c_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();
}
}
}