c# - 遍历树时使用线程

标签 c# multithreading performance parallel-processing traversal

我想加快遍历树的过程。这是一个节点的示例:

    class Node
    {
        public List<Node> Children { get; set; }
        public int SompeProperty { get; set; }
        public String SomeOtherProperty { get; set; }
    }

我遍历 try 的方式是这样的:

    static void TraverseTree(Node ParentNode)
    {
        if (ParentNode.Children == null)
            return;

        foreach (var child in ParentNode.Children)
        {
            TraverseTree(child);               
        }
    }

ParentNode.Children 方法大约需要 1 毫秒,因为 Node 表示文件或目录。我只是使用这个节点示例来更好地说明我的观点。

因此,如果您考虑一下,如果第一个节点有 4 个子节点并且每个子节点都有 10000000 个后代,我们可以提高遍历速度,如果我们在一个单独的线程中利用并行遍历这 4 个子节点中的每一个编程。如果情况是这样,那么我会采用这种方法。但是,如果我事先不知道树的结构,我怎么能做到呢?

我一直在想:

1) 开始遍历树,将前 10 个有子节点的节点放在堆栈上,然后在单独的线程上开始遍历每个节点。

2) 做类似的事情:

    static void TraverseTree(Node ParentNode)
    {
        if (ParentNode.Children == null)
            return;

        foreach (var child in ParentNode.Children)
        {
            ThreadPool.QueueUserWorkItem(new WaitCallback((x) =>
            {                    
                TraverseTree(child);   
            }), null);                            
        }
    }

这经常给我带来奇怪的结果,但速度要快得多。


结果

使用任务将算法的速度提高了大约 40% 以下是结果:

使用以下算法扫描我的整个 C:\驱动器花费了大约 5.81 秒:

        //directoryPath  = "C:\"
    var now = DateTime.Now;

        Task<List<ScanItem>> t1 = new Task<List<ScanItem>>(() =>
        {
            return GetAllFilesInDirectory(directoryPath);
        });

        t1.Start();

        t1.Wait();

        var done = DateTime.Now-now;  // done = 5.81 average

使用以下算法扫描我的整个 C:\驱动器花费了大约 3.01 秒:

        //directoryPath  = "C:\"  
        var now = DateTime.Now;


        // get all directories in my c: drive it should only contain directories
        var directories = Directory.GetDirectories(directoryPath);

        // directories = 17 directories:  inetpub, MSOCache, PrefLogs, ProgramFiles, ProgramFiles (x86) etc...

        Task<List<ScanItem>>[] myTasks = new Task<List<ScanItem>>[directories.Length];

        // create a task fore each directory in the c:\ drive
        for (int k = 0; k < myTasks.Length; k++)
        {
            var currentDir = directories[k];
            myTasks[k] = new Task<List<ScanItem>>(() =>
            {
                return GetAllFilesInDirectory(currentDir);
            });                
        }

        // start all the tasks
        for (int k = 0; k < myTasks.Length; k++)
            myTasks[k].Start();


        Task.WaitAll(myTasks); // wait for all tasks to finish

        var done = now - DateTime.Now;  // average about 3.01 seconds

如果我在哪里遍历列表,第一个算法返回 318,222 个文件和目录(这是正确的数字)。第二种算法返回 318,195,这非常接近我不明白为什么......

我正在一台有 8 个内核的计算机上对此进行测试。也许如果我在一台有 2 个内核的计算机上使用一个任务运行它可能比创建所有这 17 个任务更快。

如果您想知道我使用什么算法来快速获取文件,请查看 https://stackoverflow.com/a/724184/637142

最佳答案

使用任务并行库,而不是滚动您自己的并行代码。它非常适合解决这类问题。

TPL 的工作方式不是为问题分配线程,而是将问题分解为“任务”,然后让 TPL 负责确定如何在可用工作线程池中并行处理工作。只需为树的每个子分支创建一个任务;这些任务可以反过来为它们的子分支产生它们自己的任务。 TPL 将从池中分配线程,直到处理器饱和。

因此,让 TPL 知道您的任务是在 CPU 上还是在 I/O 上被控制是很重要的:

  • 如果任务受 CPU 限制,则 TPL 将为每个 CPU 分配一个线程池并让其他任务等待,直到有可用的内核;最大化吞吐量并使所有处理器饱和。这正是您想要的:如果您购买了一台配备四个处理器的机器,其中两个处于闲置状态,那么您为两个未使用的内核付费。

  • 如果单个任务是 I/O 绑定(bind)的,那么您可以在创建任务时使用 LongRunning 选项来向 TPL 指示该任务不应消耗整个核心;其他任务应该轮到那个核心。

  • 如果看起来是这种情况,您有许多 I/O 绑定(bind)任务,那么您应该考虑改用TaskCompletionSource,因为它允许更有效地使用“延续”回调。还可以考虑使用 C# 5 的新 async/await 功能来安排延续;它提供了一种更愉快的方式来编写异步代码。

当然,不要忘记,如果问题实际上是使机器的 I/O 能力饱和,那么再多的处理器并行性也不会产生影响。如果您要给游泳池注水,向同一个水龙头添加更多软管不会增加通过该水龙头的流量。

关于c# - 遍历树时使用线程,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/10032189/

相关文章:

java - AWS Lambda - 如果我启动一个线程,当 Lambda 卡住/解冻时会发生什么?

ios - 有没有办法检查 Objective-c 提供的方法的时间复杂度?

c# - MVC 3 DropDownFor 和 ViewModel 不工作

c - 相当于 macOS 上的 fwrite_unlocked 吗?

c# - Entity Framework - 在程序集中找不到迁移配置类型

c++ - 使用 QT5_ADD_RESOURCES 和使用 CMake 进行多线程编译时损坏的资源 .cpp 文件

python - 在非矩形二维网格上高效地找到最近点的索引

performance - FizzBu​​zz 程序似乎很慢 : why?

c# - .NET File.Create,之后无法删除文件

c# - 如何在不使用循环的情况下将行集合(通过 linq 查询获得)分配给数据表?