【问题标题】:Fraction of Ping Tasks never complete部分 Ping 任务从未完成
【发布时间】:2021-10-13 15:19:02
【问题描述】:

我正在尝试 ping 特定接口上的所有链接本地 IP 地址(“169.254.xxx.yyy”;其中大约 65000 个)。我期望只有一两次成功。 我想尽早测试活跃的 IP 地址(即在等待所有其他 ping 超时之前);如果一个活动地址是我想要的设备,那么我可以取消所有其他 ping。

我的应用程序是 C#、WinForms、async/await。 我创建了一个List<Task<T>>,每个都使用一个单独的 Ping 对象来探测一个特定的地址。然后我使用Task.WhenAny 检索结果并逐步从列表中删除相应的任务。 (我还尝试了其他更有效的按顺序处理结果的方法,结果相似)。

我发现,在大约 65000 个任务中,最完整并提供了适当的结果。但是,其中有几千个(确切的数量因运行而异)仍处于 WaitingForActivation 状态并且永远不会运行。 只有当我将任务数量减少到大约 1300 个以下时,我才能看到所有任务都正确完成;否则,我看到其中的一小部分(大约 5-10%)仍处于 WaitingForActivation 状态。未运行的任务似乎随机分布在整个列表中。

我尝试将代码移动到控制台应用程序,结果相同。 如果在每个任务中我通过调用具有相同超时时间的 Task.Delay() 来替换 SendPingAsync 的使用,则所有任务都按预期完成。

我的控制台测试应用程序:

using System;
using System.Collections.Generic;
using System.Net.NetworkInformation;
using System.Net.WebSockets;
using System.Threading.Tasks;

namespace AsyncPing
{
    class Program
    {
        static ClientWebSocket _commandWebSocket;

        static async Task Main(string[] args)
        {
            var topLevelTasks = new List<Task<ClientWebSocket>>();
            // Scanning task
            topLevelTasks.Add(Task.Run(async () =>
                await TryToConnectLinkLocal()));
            // Monitoring task (just for debugging)
            topLevelTasks.Add(Task.Run(async () => await MonitorLLTasks()));

            await Task.WhenAll(topLevelTasks);
        }

        // Monitoring Task; periodically reports on the state of the tasks collection
        private static async Task<ClientWebSocket> MonitorLLTasks()
        {
            for (int i = 0; i < 1000; ++i)
            {
                await Task.Delay(1000);

                int waitingForActivation = 0, waitingToRun = 0, running = 0, completed = 0;
                int index = 0;
                while (index < tasks.Count)
                {
                    try
                    {
                        switch (tasks[index].Status)
                        {
                            case TaskStatus.WaitingForActivation:
                                waitingForActivation++;
                                break;
                            case TaskStatus.WaitingToRun:
                                waitingToRun++;
                                break;
                            case TaskStatus.Running:
                                running++;
                                break;
                            case TaskStatus.RanToCompletion:
                                completed++;
                                break;
                        }

                        ++index;
                    }
                    catch
                    {
                        // Very occasionally, LLtasks[index] has been removed by the time we access it
                    }
                }
                Console.WriteLine($"There are {index} tasks: {waitingForActivation} waitingForActivation; {waitingToRun} waitingToRun; {running} running; {completed} completed.  {handled} results have been handled.");

                if (tasks.Count == 0)
                    break;
            }
            return null;
        }

        const string LinkLocalIPPrefix = "169.254.";

        static List<Task<String>> tasks = new List<Task<String>>();
        static int handled = 0;

        private static async Task<ClientWebSocket> TryToConnectLinkLocal()
        {
            // Link-local addresses all start with this prefix
            string baseIP = LinkLocalIPPrefix;

            tasks.Clear();
            handled = 0;

            Console.WriteLine("Scanning Link-local addresses...");
            // Scan all Link-local addresses
            // We build a task for each ip address.  
            // Note that there are nearly 65536 addresses to ping and the tasks start running
            // as soon as we start creating them. 
            for (int i = 1; i < 255; i++)
            {
                string ip_i = baseIP + i.ToString() + ".";
                for (int j = 1; j < 255; j++)
                {
                    string ip = ip_i + j.ToString();

                    var task = Task.Run(() => TryToConnectLinkLocal(ip));
                    tasks.Add(task);
                }
            }

            while (tasks.Count > 0)
            {
                var t = await Task.WhenAny(tasks);
                tasks.Remove(t);
                String result = await t;
                handled++;
            }

            return null;
        }
    
        private const int _pingTimeout = 10; // 10ms ought to be enough!

        // Ping the specified address
        static async Task<String> TryToConnectLinkLocal(string ip)
        {
            using (Ping ping = new Ping())
            {
                // This dummy code uses a fixed IP address to avoid possibility of a successful ping
                var reply = await ping.SendPingAsync("169.254.123.234", _pingTimeout);

                if (reply.Status == IPStatus.Success)
                {
                    Console.WriteLine("Response at LL address " + ip);
                    return ip;
                }
            }

            // Alternative: just wait for the duration of the timeout
            //await Task.Delay(_pingTimeout);

            return null;
        }

    }
}

典型的输出类似于(为简洁起见,编辑了类似的行):

Scanning Link-local addresses...
There are 14802 tasks: 12942 waitingForActivation; 0 waitingToRun; 0 running; 1860 completed.  0 results have been handled.
There are 24623 tasks: 20005 waitingForActivation; 0 waitingToRun; 0 running; 4618 completed.  0 results have been handled.
There are 27287 tasks: 21170 waitingForActivation; 0 waitingToRun; 0 running; 6117 completed.  0 results have been handled.
There are 41714 tasks: 32471 waitingForActivation; 0 waitingToRun; 0 running; 9243 completed.  0 results have been handled.
There are 51263 tasks: 38816 waitingForActivation; 0 waitingToRun; 0 running; 12447 completed.  0 results have been handled.
There are 63891 tasks: 48403 waitingForActivation; 0 waitingToRun; 0 running; 15488 completed.  0 results have been handled.
There are 64498 tasks: 46496 waitingForActivation; 0 waitingToRun; 0 running; 18002 completed.  18 results have been handled.

<All tasks have been created. Many have been run.  More and more results are handled and the corresponding tasks removed>

There are 6626 tasks: 5542 waitingForActivation; 0 waitingToRun; 0 running; 1084 completed.  57890 results have been handled.
There are 5542 tasks: 5542 waitingForActivation; 0 waitingToRun; 0 running; 0 completed.  58974 results have been handled.
There are 5542 tasks: 5542 waitingForActivation; 0 waitingToRun; 0 running; 0 completed.  58974 results have been handled.
There are 5542 tasks: 5542 waitingForActivation; 0 waitingToRun; 0 running; 0 completed.  58974 results have been handled.
There are 5542 tasks: 5542 waitingForActivation; 0 waitingToRun; 0 running; 0 completed.  58974 results have been handled.
There are 5542 tasks: 5542 waitingForActivation; 0 waitingToRun; 0 running; 0 completed.  58974 results have been handled.
There are 5542 tasks: 5542 waitingForActivation; 0 waitingToRun; 0 running; 0 completed.  58974 results have been handled.

<5542 results remain in the list because they are stuck WaitingForActivation.  Only 58974 results (of 64516) have been handled.  This state continues indefinitely>

我很乐意收到对此行为的解释、如何修复它的建议和/或有关如何以更有效的方式探测网络的建议。

我重命名了这个问题,因为我从 Stephen Cleary 的 blog 了解到,这些任务可能是 Promise 任务,它以 WaitingForActivation 状态开始。然而,这里真正重要的是它们永远不会完成。

自从原始帖子以来,我尝试了以下方法:

  • 按照 Stephen Toub 的article 使用延续任务按完成顺序处理结果;
  • 使用ConcurrentExclusiveSchedulerPair 尝试限制任务的执行;
  • 使用ConfigureAwait(false) 尝试改变使用的SynchronizationContext
  • 据我所知,检查是否没有抛出异常。

这些似乎都没有任何显着影响。

我还尝试将扫描分成多个子扫描。子扫描生成许多异步执行的 Ping 任务(根据上面的代码)。每个子扫描都是同步执行的,一个接一个。在我的机器上,只要我将 Ping 任务的数量保持在 1100 以下,它们都可以正确执行。除此之外,其中的一小部分永远不会完成。似乎这种方法并没有那么慢(大概是因为网络接口被淹没超过一定数量的同时 ping),所以它为我的问题提供了一种实用的方法。但是,为什么某些任务无法完成超过 1100 个任务的问题仍然存在。仍然:如果我通过调用 await Task.Delay(...) 来替换 Ping,则所有任务都会完成。

【问题讨论】:

    标签: c# async-await network-programming


    【解决方案1】:

    我的建议是使用TPL Dataflow 库中的ActionBlock&lt;T&gt;。该组件将负责在找到第一个成功的IPAddress 时及时取消该过程,同时执行最大并发策略。

    首先实例化一个ActionBlock&lt;IPAddress&gt;,提供将为每个IPAddress 运行的操作,以及执行选项。然后使用Post 方法为块提供地址。然后,您通过调用Complete 方法发出不再发布地址的信号。最后你 await 组件的 Completion 属性。示例:

    const int pingTimeout = 10;
    using var cts = new CancellationTokenSource();
    IPAddress result = null;
    
    var block = new ActionBlock<IPAddress>(async address =>
    {
        try
        {
            var reply = await new Ping().SendPingAsync(address, pingTimeout);
            if (reply.Status == IPStatus.Success)
            {
                Interlocked.CompareExchange(ref result, address, null);
                cts.Cancel();
            }
        }
        catch (PingException) { } // Ignore
    }, new ExecutionDataflowBlockOptions()
    {
        MaxDegreeOfParallelism = 100, // Select a reasonable value
        CancellationToken = cts.Token
    });
    
    byte b1 = 169, b2 = 254;
    var addresses = Enumerable.Range(0, 255)
        .SelectMany(_ => Enumerable.Range(0, 255),
            (b3, b4) => new IPAddress(
                new byte[] { b1, b2, (byte)b3, (byte)b4 }));
    
    foreach (var address in addresses) block.Post(address);
    block.Complete();
    
    try { await block.Completion; }
    catch (OperationCanceledException) { } // Ignore
    
    Console.WriteLine($"Result: {result?.ToString() ?? "(not found)"}");
    

    【讨论】:

    • 谢谢。我不熟悉 TPL Dataflow 库,但从表面上看,这似乎是一种非常合理的方法 - 我会试一试。它仍然留下一个问题,为什么我所做的不是不是一个合理的方法,请注意......
    • @PhilAtkin 老实说我没有深入研究你的代码,所以我不能说它有什么问题。关于 TPL 数据流,这听起来很奇特,但它实际上嵌入在 .NET Core / .NET 5 平台中。它更常用于构建管道(链接在一起的多个块),但它也可用于利用单个块的功能(如上例所示)。如果您在几周后提出这个问题,我会建议使用 Parallel.ForEachAsync,它将与 .NET 6 一起发布,也可以解决您的问题。
    • 有趣!我试过了,结果几乎和我的(分块)版本一样。块大小和 MaxDegreeOfParallelism 为 100 时,所用时间约为 5 分 30 秒。有 1000 个,两者都在 30 多秒内运行。对于 1200,都无法完成。我想这并不完全令人惊讶,因为它们可能基于相同的底层技术。但它鼓励我相信未能完成不是我造成的(或你的!)。
    • @PhilAtkin 感谢分享此信息!如果您发现SendPingAsync 在并行化过多时失败的更深层次原因,您可以将其发布在as an answer
    • 令人不满意(我喜欢追根究底),但现在我有一个可行的解决方案(至少在我的机器上;我正在其他人身上测试它)我可能会不得不让这个问题悬而未决。我担心“太多”的并行性可能会因情况而异(例如,我们知道如果 Ping 不发生,并行性会更高)。非常感谢您的贡献。
    猜你喜欢
    • 2022-01-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-09-09
    相关资源
    最近更新 更多