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