Ping任务无法完成。

22

我正在开发一个“心跳”应用程序,通过循环每分钟ping数百个IP地址。这些IP地址存储在一个名为Machines的类列表中。我有一个循环来创建Task<MachinePingResults>(其中MachinePingResults基本上是IP和在线状态的元组),并调用使用System.Net.NetworkInformation的ping函数。

问题是,在运行数小时(或数天)后,主程序的某个循环无法完成Tasks,从而导致内存泄漏。我无法确定为什么我的任务没有完成(如果我在运行时查看任务列表,经过几天运行后,有数百个任务显示为“等待”)。大多数情况下,所有任务都会完成并被处理;只是偶尔它们不会完成。例如,过去24小时中有一个问题,大约在12小时时出现了148个未完成的等待任务。由于无法查看Ping挂起的原因(因为它是.NET内部的),我无法重现此问题进行调试。

(据显示,如果存在DNS问题,则.NET中的Ping调用可能会挂起,并且内置超时会失败,这就是为什么我构建了一个额外的超时)

如果ping在15秒内没有返回,我有一种方法可以取消主循环,使用Task.DelayCancellationToken。然后,在每个Ping函数中,我都有一个Delay,以防Ping调用本身挂起导致函数强制完成。还要注意,我只ping IPv4;没有IPv6或URL。

主循环

pingcancel = new CancellationTokenSource();

List<Task<MachinePingResults>> results = new List<Task<MachinePingResults>>();

try
{
    foreach (var m in localMachines.FindAll(m => !m.Online))
        results.Add(Task.Run(() =>
            PingMachine(m.ipAddress, 8000), pingcancel.Token
        ));

    await Task.WhenAny(Task.WhenAll(results.ToArray()), Task.Delay(15000));

    pingcancel.Cancel();
}
catch (Exception ex) { Console.WriteLine(ex); }
finally
{
    results.Where(r => r.IsCompleted).ToList()
        .ForEach(r =>
        //modify the online machines);
        results.Where(r => r.IsCompleted).ToList().ForEach(r => r.Dispose());
        results.Clear();
 }

ping功能

static async Task<MachinePingResults> PingMachine(string ipaddress, int timeout)
{
    try
    {
        using (Ping ping = new Ping())
        {
            var reply = ping.SendPingAsync(ipaddress, timeout);

            await Task.WhenAny(Task.Delay(timeout), reply);

            if (reply.IsCompleted && reply.Result.Status == IPStatus.Success)
            {
                return new MachinePingResults(ipaddress, true);
            }
        }
    }
    catch (Exception ex)
    {
        Debug.WriteLine("Error: " + ex.Message);
    }
    return new MachinePingResults(ipaddress, false);
}

每个 Task 都有一个延迟以便在 Ping 挂起时继续执行,但我不知道是什么原因导致一些 Task<MachinePingResults> 永远无法完成。

如何确保使用 .NET PingTask 结束?

使用 .NET 5.0,在运行 Windows 10 和 Windows Server 2012 的机器上出现问题。

在 VS Tasks 窗口中等待的任务列表图像

在 VS Tasks 窗口中等待的任务列表图像


1
@TheodorZoulias 这是随机的;有时我会ping所有的1391个IP,有时只有几百个。由于它会在所有IP上挂起,我认为这与IP本身无关。我想如果内部的.NET Ping抛出异常,它会挂起函数和任务。 - davidsbro
3
你面临一个棘手的问题需要解决。在你的情况下,我会尝试通过使用Parallel.ForEachAsync或类似方法来限制并发SendPingAsync操作的数量。不过,我并不指望这个方法能够解决问题。 - Theodor Zoulias
2
Task.WhenAny已经是修复它的一种尝试了吗?考虑到ping有自己的超时,PingMachine方法中的许多内容似乎是多余的。 - GolezTrol
13
这个问题正在元社区上讨论。 - justANewb stands with Ukraine
2
在我看来,可能的挂起是由于 DNS 引起的,因为 Ping.SendAsync 使用同步的 Dns.GetAddresses,即使它应该使用异步。因此,将这两个操作分开并手动调用 Dns.GetHostAddressesAsync,然后仅将 IP 地址传递给 Ping,这样做可能是值得的。您可以在 Framework 4 的参考源代码 中看到此错误,并在 Runtime 5.0+ 中看到修复方法。 - Charlieface
显示剩余11条评论
1个回答

7

这里发布的代码存在相当多的漏洞,但是我试图复制并在此过程中进行了一些重构。

这个版本看起来非常稳健,实际调用SendAsync的操作被包装在一个适配器类中。

我承认这并不一定直接回答了问题,但在无法完全复制您的问题的情况下,提供了一种结构代码的替代方式,可能可以消除问题。

    async Task Main()
    {
        var masterCts = new CancellationTokenSource(TimeSpan.FromSeconds(15)); // 15s overall timeout
        
        var localMachines = new List<LocalMachine>
        {       
            new LocalMachine("192.0.0.1", false), // Should be not known - TimedOut
            new LocalMachine("192.168.86.88", false), // Should be not known - DestinationHostUnreachable (when timeout is 8000)
            new LocalMachine("www.dfdfsdfdfdsgrdf.cdcc", false), // Should be not known - status Unknown because of PingException
            new LocalMachine("192.168.86.87", false) // Known - my local IP
        };

        var results = new List<PingerResult>();

        try
        {
            // Create the "hot" tasks
            var tasks = localMachines.Where(m => !m.Online)
                                    .Select(m => new Pinger().SendPingAsync(m.HostOrAddress, 8000, masterCts.Token))
                                    .ToArray();

            await Task.WhenAll(tasks);
            
            results.AddRange(tasks.Select(t => t.Result));
        }
        finally
        {
            results.ForEach(r => localMachines.Single(m => m.HostOrAddress.Equals(r.HostOrAddress)).Online = r.Status == IPStatus.Success);

            results.Dump();  // For LINQPad
            localMachines.Dump(); // For LINQPad

            results.Clear();
        }
    }

    public class LocalMachine
    {
        public LocalMachine(string hostOrAddress, bool online)
        {
            HostOrAddress = hostOrAddress;
            Online = online;
        }

        public string HostOrAddress { get; }

        public bool Online { get; set; }
    }

    public class PingerResult
    {
        public string HostOrAddress {get;set;}
        
        public IPStatus Status {get;set;}
    }

    public class Pinger 
    {
        public async Task<PingerResult> SendPingAsync(string hostOrAddress, int timeout, CancellationToken token)
        {
            // Check if timeout has occurred
            token.ThrowIfCancellationRequested();

            IPStatus status = default;

            try
            {
                var reply = await SendPingInternal(hostOrAddress, timeout, token);
                status = reply.Status;
            }
            catch (PingException)
            {               
                status = IPStatus.Unknown;
            }
            
            return new PingerResult
            {
                HostOrAddress = hostOrAddress,
                Status = status
            };
        }

        // Wrap the legacy EAP pattern offered by Ping.
        private Task<PingReply> SendPingInternal(string hostOrAddress, int timeout, CancellationToken cancelToken)
        {
            var tcs = new TaskCompletionSource<PingReply>();

            if (cancelToken.IsCancellationRequested)
            {
                tcs.TrySetCanceled();
            }   
            else
            {
                using (var ping = new Ping())
                {
                    ping.PingCompleted += (object sender, PingCompletedEventArgs e) =>
                    {
                        if (!cancelToken.IsCancellationRequested)
                        {
                            if (e.Cancelled)
                            {
                                tcs.TrySetCanceled();
                            }
                            else if (e.Error != null)
                            {
                                tcs.TrySetException(e.Error);
                            }
                            else
                            {
                                tcs.TrySetResult(e.Reply);
                            }
                        }
                    };
                    
                    cancelToken.Register(() => { tcs.TrySetCanceled(); });

                    ping.SendAsync(hostOrAddress, timeout, new object());
                }
            };

            return tcs.Task;
        }
    }

编辑:

我在评论区注意到你提到了要ping “all 1391”。这时候,我会使用SemaphoreSlim来限制发送ping的并发数。详情请参考这篇博客文章(发布于很久以前!),其中介绍了实现的方法:https://devblogs.microsoft.com/pfxteam/implementing-a-simple-foreachasync/


谢谢你所付出的努力。我会尝试这个方法,但是我想提一下,由于问题需要一些时间才能出现,所以在我确认这是否是答案之前可能需要几天时间。 - davidsbro
@davidsbro - 你的问题没有提到这一点;它应该被提及。 - Security Hound
1
@安全猎犬 第二段... - davidsbro
@davidsbro 没问题。如果你需要ping超过一千台机器,我添加了一个使用信号量进行限流的编辑建议。 - Paul Suart
好的。没有看到你的代码很难说。在我的代码中不存在“TranslateTasktoEAP”。 - Paul Suart
显示剩余3条评论

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