为什么 Parallel.ForEach 比 AsParallel().ForAll() 快得多,尽管 MSDN 另有建议?

2023-12-09

我一直在做一些调查,看看如何创建一个通过树运行的多线程应用程序。

为了找到如何以最佳方式实现这一点,我创建了一个测试应用程序,该应用程序在我的 C:\ 磁盘上运行并打开所有目录。

class Program
{
    static void Main(string[] args)
    {
        //var startDirectory = @"C:\The folder\RecursiveFolder";
        var startDirectory = @"C:\";

        var w = Stopwatch.StartNew();

        ThisIsARecursiveFunction(startDirectory);

        Console.WriteLine("Elapsed seconds: " + w.Elapsed.TotalSeconds);

        Console.ReadKey();
    }

    public static void ThisIsARecursiveFunction(String currentDirectory)
    {
        var lastBit = Path.GetFileName(currentDirectory);
        var depth = currentDirectory.Count(t => t == '\\');
        //Console.WriteLine(depth + ": " + currentDirectory);

        try
        {
            var children = Directory.GetDirectories(currentDirectory);

            //Edit this mode to switch what way of parallelization it should use
            int mode = 3;

            switch (mode)
            {
                case 1:
                    foreach (var child in children)
                    {
                        ThisIsARecursiveFunction(child);
                    }
                    break;
                case 2:
                    children.AsParallel().ForAll(t =>
                    {
                        ThisIsARecursiveFunction(t);
                    });
                    break;
                case 3:
                    Parallel.ForEach(children, t =>
                    {
                        ThisIsARecursiveFunction(t);
                    });
                    break;
                default:
                    break;
            }

        }
        catch (Exception eee)
        {
            //Exception might occur for directories that can't be accessed.
        }
    }
}

然而,我遇到的是,当在模式 3 (Parallel.ForEach) 下运行时,代码在大约 2.5 秒内完成(是的,我有一个 SSD ;))。在没有并行化的情况下运行代码大约需要 8 秒。在模式 2 (AsParalle.ForAll()) 下运行代码会花费近乎无限的时间。

在检查进程资源管理器时,我还遇到了一些奇怪的事实:

Mode1 (No Parallelization):
Cpu:     ~25%
Threads: 3
Time to complete: ~8 seconds

Mode2 (AsParallel().ForAll()):
Cpu:     ~0%
Threads: Increasing by one per second (I find this strange since it seems to be waiting on the other threads to complete or a second timeout.)
Time to complete: 1 second per node so about 3 days???

Mode3 (Parallel.ForEach()):
Cpu:     100%
Threads: At most 29-30
Time to complete: ~2.5 seconds

我发现特别奇怪的是 Parallel.ForEach 似乎忽略了仍在运行的任何父线程/任务,而 AsParallel().ForAll() 似乎等待上一个任务完成(这不会很快,因为所有父任务仍在等待子任务完成)。

我在 MSDN 上读到的内容是:“在可能的情况下,优先选择 ForAll 而不是 ForEach”

Source: http://msdn.microsoft.com/en-us/library/dd997403(v=vs.110).aspx

有谁知道为什么会这样?

Edit 1:

按照 Matthew Watson 的要求,我首先将树加载到内存中,然后再循环遍历它。现在树的加载是按顺序完成的。

但结果是一样的。 Unparallelized 和 Parallel.ForEach 现在在大约 0.05 秒内完成整个树,而 AsParallel().ForAll 仍然每秒只执行大约 1 步。

Code:

class Program
{
    private static DirWithSubDirs RootDir;

    static void Main(string[] args)
    {
        //var startDirectory = @"C:\The folder\RecursiveFolder";
        var startDirectory = @"C:\";

        Console.WriteLine("Loading file system into memory...");
        RootDir = new DirWithSubDirs(startDirectory);
        Console.WriteLine("Done");


        var w = Stopwatch.StartNew();

        ThisIsARecursiveFunctionInMemory(RootDir);

        Console.WriteLine("Elapsed seconds: " + w.Elapsed.TotalSeconds);

        Console.ReadKey();
    }        

    public static void ThisIsARecursiveFunctionInMemory(DirWithSubDirs currentDirectory)
    {
        var depth = currentDirectory.Path.Count(t => t == '\\');
        Console.WriteLine(depth + ": " + currentDirectory.Path);

        var children = currentDirectory.SubDirs;

        //Edit this mode to switch what way of parallelization it should use
        int mode = 2;

        switch (mode)
        {
            case 1:
                foreach (var child in children)
                {
                    ThisIsARecursiveFunctionInMemory(child);
                }
                break;
            case 2:
                children.AsParallel().ForAll(t =>
                {
                    ThisIsARecursiveFunctionInMemory(t);
                });
                break;
            case 3:
                Parallel.ForEach(children, t =>
                {
                    ThisIsARecursiveFunctionInMemory(t);
                });
                break;
            default:
                break;
        }
    }
}

class DirWithSubDirs
{
    public List<DirWithSubDirs> SubDirs = new List<DirWithSubDirs>();
    public String Path { get; private set; }

    public DirWithSubDirs(String path)
    {
        this.Path = path;
        try
        {
            SubDirs = Directory.GetDirectories(path).Select(t => new DirWithSubDirs(t)).ToList();
        }
        catch (Exception eee)
        {
            //Ignore directories that can't be accessed
        }
    }
}

Edit 2:

阅读马修评论的更新后,我尝试将以下代码添加到程序中:

ThreadPool.SetMinThreads(4000, 16);
ThreadPool.SetMaxThreads(4000, 16);

然而,这不会改变 AsParallel 的执行方式。前 8 个步骤仍在瞬间执行,然后减慢至 1 步/秒。

(额外注意,我目前忽略了当我无法通过 Directory.GetDirectories() 周围的 Try Catch 块访问目录时发生的异常)

Edit 3:

另外,我主要感兴趣的是 Parallel.ForEach 和 AsParallel.ForAll 之间的区别,因为对我来说,奇怪的是,出于某种原因,第二个为它执行的每个递归创建一个线程,而第一个一次处理大约 30 个线程中的所有内容最大限度。 (也是为什么 MSDN 建议使用 AsParallel,即使它创建了如此多的线程且超时时间约为 1 秒)

Edit 4:

我还发现了另外一件奇怪的事情: 当我尝试将线程池上的 MinThreads 设置为高于 1023 时,它似乎忽略了该值并缩小到 8 或 16 左右: ThreadPool.SetMinThreads(1023, 16);

不过,当我使用 1023 时,它会非常快地执行前 1023 个元素,然后又回到我一直经历的慢速。

注意:实际上现在创建了超过 1000 个线程(相比之下,整个 Parallel.ForEach 线程创建了 30 个)。

这是否意味着 Parallel.ForEach 在处理任务方面更加智能?

更多信息,当您将值设置为 1023 以上时,此代码会打印两次 8 - 8:(当您将值设置为 1023 或更低时,它会打印正确的值)

        int threadsMin;
        int completionMin;
        ThreadPool.GetMinThreads(out threadsMin, out completionMin);
        Console.WriteLine("Cur min threads: " + threadsMin + " and the other thing: " + completionMin);

        ThreadPool.SetMinThreads(1023, 16);
        ThreadPool.SetMaxThreads(1023, 16);

        ThreadPool.GetMinThreads(out threadsMin, out completionMin);
        Console.WriteLine("Now min threads: " + threadsMin + " and the other thing: " + completionMin);

Edit 5:

根据 Dean 的要求,我创建了另一个案例来手动创建任务:

case 4:
    var taskList = new List<Task>();
    foreach (var todo in children)
    {
        var itemTodo = todo;
        taskList.Add(Task.Run(() => ThisIsARecursiveFunctionInMemory(itemTodo)));
    }
    Task.WaitAll(taskList.ToArray());
    break;

这也与 Parallel.ForEach() 循环一样快。所以我们仍然不知道为什么 AsParallel().ForAll() 慢得多。


这个问题非常容易调试,当您遇到线程问题时,这是一种罕见的奢侈。这里的基本工具是“调试”>“窗口”>“线程”调试器窗口。显示活动线程并让您查看它们的堆栈跟踪。你会很容易地看到,一旦速度变慢,你就会有dozens全部被卡住的活动线程数。他们的堆栈跟踪看起来都一样:

    mscorlib.dll!System.Threading.Monitor.Wait(object obj, int millisecondsTimeout, bool exitContext) + 0x16 bytes  
    mscorlib.dll!System.Threading.Monitor.Wait(object obj, int millisecondsTimeout) + 0x7 bytes 
    mscorlib.dll!System.Threading.ManualResetEventSlim.Wait(int millisecondsTimeout, System.Threading.CancellationToken cancellationToken) + 0x182 bytes    
    mscorlib.dll!System.Threading.Tasks.Task.SpinThenBlockingWait(int millisecondsTimeout, System.Threading.CancellationToken cancellationToken) + 0x93 bytes   
    mscorlib.dll!System.Threading.Tasks.Task.InternalRunSynchronously(System.Threading.Tasks.TaskScheduler scheduler, bool waitForCompletion) + 0xba bytes  
    mscorlib.dll!System.Threading.Tasks.Task.RunSynchronously(System.Threading.Tasks.TaskScheduler scheduler) + 0x13 bytes  
    System.Core.dll!System.Linq.Parallel.SpoolingTask.SpoolForAll<ConsoleApplication1.DirWithSubDirs,int>(System.Linq.Parallel.QueryTaskGroupState groupState, System.Linq.Parallel.PartitionedStream<ConsoleApplication1.DirWithSubDirs,int> partitions, System.Threading.Tasks.TaskScheduler taskScheduler) Line 172  C#
// etc..

每当你看到这样的事情时,你应该立即想到消防水带问题。可能是继争用和死锁之后第三个最常见的线程错误。

现在您已经知道了原因,您可以推断出,代码的问题在于每个完成的线程都会添加 N 个线程。其中N是目录中子目录的平均数量。实际上,线程数量增加了指数地,这总是不好的。只有当 N = 1 时它才会保持控制,当然这在典型的磁盘上永远不会发生。

请注意,就像几乎所有线程问题一样,这种不当行为往往会很糟糕地重复出现。您机器中的 SSD 往往会隐藏它。您机器中的 RAM 也是如此,程序很可能在您第二次运行时快速且无故障地完成。由于您现在将从文件系统缓存而不是磁盘中读取,速度非常快。修补 ThreadPool.SetMinThreads() 也隐藏了它,但它无法修复它。它永远不会解决任何问题,它只会隐藏它们。因为无论发生什么情况,指数数总是会压倒设定的最小线程数。您只能希望它在此之前完成驱动器迭代。对于拥有大驱动器的用户来说,希望渺茫。

ParallelEnumerable.ForAll() 和 Parallel.ForEach() 之间的区别现在也许也很容易解释。您可以从堆栈跟踪中看出 ForAll() 做了一些顽皮的事情,RunSynchronously() 方法会阻塞,直到所有线程完成为止。阻塞是线程池线程不应该做的事情,它会搞乱线程池并且不允许它为另一个作业调度处理器。您观察到的效果是,线程池很快就会被等待其他 N 个线程完成的线程淹没。但这并没有发生,他们正在池中等待,并且没有被安排,因为已经有很多人处于活动状态。

这是一种死锁场景,非常常见,但线程池管理器有一个解决方法。它监视活动线程池线程,并在它们未及时完成时介入。然后它允许extra要启动的线程,比 SetMinThreads() 设置的最小值多 1 个。但不能超过 SetMaxThreads() 设置的最大值,过多的活动 tp 线程是有风险的,并且可能会触发 OOM。这确实解决了死锁,它完成了 ForAll() 调用之一。但这发生的速度非常慢,线程池每秒只执行两次。在它赶上之前你就会失去耐心。

Parallel.ForEach() 没有这个问题,它不会阻塞,因此不会堵塞池。

似乎是解决方案,但请记住,您的程序仍在占用机器的内存,向池中添加更多的等待线程。这也可能使您的程序崩溃,只是不太可能,因为您有大量内存,而线程池不会使用大量内存来跟踪请求。然而有些程序员也能做到这一点.

解决方案很简单,只要不使用线程即可。这是harmful,当您只有一个磁盘时,就没有并发性。确实如此not就像被多个线程征用一样。尤其是在主轴驱动器上,磁头寻道非常非常慢。 SSD 做得更好,但仍然需要 50 微秒的时间,这是您不想要或不需要的开销。访问磁盘的理想线程数始终是您无法期望得到良好缓存的磁盘one.

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

为什么 Parallel.ForEach 比 AsParallel().ForAll() 快得多,尽管 MSDN 另有建议? 的相关文章

  • 根据属性的类型使用文本框或复选框

    如果我有这样的结构 public class Parent public string Name get set public List
  • 在一个数据访问层中处理多个连接字符串

    我有一个有趣的困境 我目前有一个数据访问层 它必须与多个域一起使用 并且每个域都有多个数据库存储库 具体取决于所调用的存储过程 目前 我只需使用 SWITCH 语句来确定应用程序正在运行的计算机 并从 Web config 返回适当的连接字
  • 机器Epsilon精度差异

    我正在尝试计算 C 中双精度数和浮点数的机器 epsilon 值 作为学校作业的一部分 我在 Windows 7 64 位中使用 Cygwin 代码如下 include
  • 如何在 C# 中打开 Internet Explorer 属性窗口

    我正在开发一个 Windows 应用程序 我必须向用户提供一种通过打开 IE 设置窗口来更改代理设置的方法 Google Chrome 使用相同的方法 当您尝试更改 Chrome 中的代理设置时 它将打开 Internet Explorer
  • WPF 数据绑定到复合类模式?

    我是第一次尝试 WPF 并且正在努力解决如何将控件绑定到使用其他对象的组合构建的类 例如 如果我有一个由两个单独的类组成的类 Comp 为了清楚起见 请注意省略的各种元素 class One int first int second cla
  • 两个静态变量同名(两个不同的文件),并在任何其他文件中 extern 其中一个

    在一个文件中将变量声明为 static 并在另一个文件中进行 extern 声明 我认为这会在链接时出现错误 因为 extern 变量不会在任何对象中看到 因为在其他文件中声明的变量带有限定符 static 但不知何故 链接器 瑞萨 没有显
  • WcfSvcHost 的跨域异常

    对于另一个跨域问题 我深表歉意 我一整天都在与这个问题作斗争 现在已经到了沸腾的地步 我有一个 Silverlight 应用程序项目 SLApp1 一个用于托管 Silverlight SLApp1 Web 的 Web 项目和 WCF 项目
  • C 编程:带有数组的函数

    我正在尝试编写一个函数 该函数查找行为 4 列为 4 的二维数组中的最大值 其中二维数组填充有用户输入 我知道我的主要错误是函数中的数组 但我不确定它是什么 如果有人能够找到我出错的地方而不是编写新代码 我将不胜感激 除非我刚去南方 我的尝
  • LINQ:使用 INNER JOIN、Group 和 SUM

    我正在尝试使用 LINQ 执行以下 SQL 最接近的是执行交叉联接和总和计算 我知道必须有更好的方法来编写它 所以我向堆栈团队寻求帮助 SELECT T1 Column1 T1 Column2 SUM T3 Column1 AS Amoun
  • C# 动态/expando 对象的深度/嵌套/递归合并

    我需要在 C 中 合并 2 个动态对象 我在 stackexchange 上找到的所有内容仅涵盖非递归合并 但我正在寻找能够进行递归或深度合并的东西 非常类似于jQuery 的 extend obj1 obj2 http api jquer
  • 为什么 isnormal() 说一个值是正常的,而实际上不是?

    include
  • C 函数 time() 如何处理秒的小数部分?

    The time 函数将返回自 1970 年以来的秒数 我想知道它如何对返回的秒数进行舍入 例如 对于100 4s 它会返回100还是101 有明确的定义吗 ISO C标准没有说太多 它只说time 回报 该实现对当前日历时间的最佳近似 结
  • 编译时展开 for 循环内的模板参数?

    维基百科 here http en wikipedia org wiki Template metaprogramming Compile time code optimization 给出了 for 循环的编译时展开 我想知道我们是否可以
  • C++ 继承的内存布局

    如果我有两个类 一个类继承另一个类 并且子类仅包含函数 那么这两个类的内存布局是否相同 e g class Base int a b c class Derived public Base only functions 我读过编译器无法对数
  • 使用特定参数从 SQL 数据库填充组合框

    我在使用参数从 sql server 获取特定值时遇到问题 任何人都可以解释一下为什么它在 winfom 上工作但在 wpf 上不起作用以及我如何修复它 我的代码 private void UpdateItems COMBOBOX1 Ite
  • C++ 中的 include 和 using 命名空间

    用于使用cout 我需要指定两者 include
  • C# 使用“?” if else 语句设置值这叫什么

    嘿 我刚刚看到以下声明 return name null name NA 我只是想知道这在 NET 中叫什么 是吗 代表即然后执行此操作 这是一个俗称的 条件运算符 三元运算符 http en wikipedia org wiki Tern
  • DotNetZip:如何提取文件,但忽略zip文件中的路径?

    尝试将文件提取到给定文件夹 忽略 zip 文件中的路径 但似乎没有办法 考虑到其中实现的所有其他好东西 这似乎是一个相当基本的要求 我缺少什么 代码是 using Ionic Zip ZipFile zf Ionic Zip ZipFile
  • MySQL Connector C/C API - 使用特殊字符进行查询

    我是一个 C 程序 我有一个接受域名参数的函数 void db domains query char name 使用 mysql query 我测试数据库中是否存在域名 如果不是这种情况 我插入新域名 char query 400 spri
  • 指针和内存范围

    我已经用 C 语言编程有一段时间了 但对 C 语言还是很陌生 有时我对 C 处理内存的方式感到困惑 考虑以下有效的 C 代码片段 const char string void where is this pointer variable l

随机推荐