任务并行不稳定,有时使用100%CPU

And*_*Wei 9 c# parallel-processing cpu-usage

我目前正在测试Parallel for C#.通常它工作正常,使用并行比正常的foreach循环更快.但是,有时(如5次中的1次),我的CPU将达到100%的使用率,导致并行任务非常慢.我的CPU设置为i5-4570,内存为8gb.有谁知道为什么会出现这个问题?

以下是我用来测试功能的代码

            // Using normal foreach
            ConcurrentBag<int> resultData = new ConcurrentBag<int>();
            Stopwatch sw = new Stopwatch();
            sw.Start();
            foreach (var item in testData)
            {
                if (item.Equals(1))
                {
                    resultData.Add(item);
                }
            }
            Console.WriteLine("Normal ForEach " + sw.ElapsedMilliseconds);

            // Using list parallel for
            resultData = new ConcurrentBag<int>();
            sw.Restart();
            System.Threading.Tasks.Parallel.For(0, testData.Count() - 1, (i, loopState) =>
            {
                int data = testData[i];
                if (data.Equals(1))
                {
                    resultData.Add(data);
                }
            });
            Console.WriteLine("List Parallel For " + sw.ElapsedMilliseconds);

            // Using list parallel foreach
            //resultData.Clear();
            resultData = new ConcurrentBag<int>();
            sw.Restart();
            System.Threading.Tasks.Parallel.ForEach(testData, (item, loopState) =>
            {
                if (item.Equals(1))
                {
                    resultData.Add(item);
                }
            });
            Console.WriteLine("List Parallel ForEach " + sw.ElapsedMilliseconds);

            // Using concurrent parallel for 
            ConcurrentStack<int> resultData2 = new ConcurrentStack<int>();
            sw.Restart();
            System.Threading.Tasks.Parallel.For(0, testData.Count() - 1, (i, loopState) =>
            {
                int data = testData[i];
                if (data.Equals(1))
                {
                    resultData2.Push(data);
                }
            });
            Console.WriteLine("Concurrent Parallel For " + sw.ElapsedMilliseconds);

            // Using concurrent parallel foreach
            resultData2.Clear();
            sw.Restart();
            System.Threading.Tasks.Parallel.ForEach(testData, (item, loopState) =>
            {
                if (item.Equals(1))
                {
                    resultData2.Push(item);
                }
            });
            Console.WriteLine("Concurrent Parallel ForEach " + sw.ElapsedMilliseconds);
Run Code Online (Sandbox Code Playgroud)

正常输出

正常ForEach 493

列表并行为315

List Parallel ForEach 328

并行并行286

并行并行ForEach 292

在100%CPU使用期间

正常ForEach 476

列表并行为8047

List Parallel ForEach 276

并行并行281

并行ForEach 3960

(这可以在任何并行任务期间发生,上面只有一个实例)

更新

通过使用@willaien提供的PLINQ方法并运行100次,不再出现此问题.我仍然不知道为什么这个问题会在第一时间出现.

var resultData3 = testData.AsParallel().Where(x => x == 1).ToList();
Run Code Online (Sandbox Code Playgroud)

Lua*_*aan 9

首先,小心Parallel- 它不会保护您免受线程安全问题的影响。在您的原始代码中,您在填充结果列表时使用了非线程安全代码。通常,您希望避免共享任何状态(尽管在这种情况下对列表进行只读访问是没问题的)。如果您真的想使用Parallel.ForParallel.ForEach过滤和聚合(AsParallel在这些情况下确实是您想要的),您应该使用线程本地状态的重载 - 您将在localFinally委托中进行最终结果聚合(请注意,它仍然是在不同的线程上运行,因此您需要确保线程安全;但是,在这种情况下,锁定很好,因为您每个线程只执行一次,而不是在每次迭代中执行此操作)。

现在,尝试解决此类问题的第一件事显然是使用分析器。所以我已经这样做了。结果如下:

  • 在这两种解决方案中几乎没有任何内存分配。即使对于相对较小的测试数据(我在测试时使用了 1M、10M 和 100M 的整数),它们与初始测试数据分配完全相形见绌。
  • 正在完成的工作是在 egParallel.ForParallel.ForEachbody 本身中,而不是在您的代码中(简单的if (data[i] == 1) results.Add(data[i]))。

第一个意味着我们可以说 GC 可能不是罪魁祸首。确实,它没有任何运行的机会。第二个更奇怪 - 这意味着在某些情况下, 的开销Parallel是出格的 - 但它似乎是随机的,有时它可以顺利运行,有时需要半秒钟。这通常会指向 GC,但我们已经排除了这一点。

我试过在没有循环状态的情况下使用重载,但这没有帮助。我试过限制MaxDegreeOfParallelism,但它只会伤害事情。现在,很明显,这段代码完全由缓存访问主导——几乎没有任何 CPU 工作,也没有 I/O——这总是有利于单线程解决方案;但即使使用MaxDegreeOfParallelism1 没有帮助 - 实际上,2 似乎是我系统上最快的。更多是无用的——同样,缓存访问占主导地位。它仍然很好奇 - 我正在使用服务器 CPU 进行测试,它一次为所有数据提供了大量缓存,虽然我们没有进行 100% 顺序访问(这几乎完全消除了延迟),它应该足够连续。无论如何,我们在单线程解决方案中有内存吞吐量的基线,并且当它运行良好时它非常接近并行化案例的速度(并行化,我读取的运行时间比单线程少 40%,在一个四核服务器 CPU 用于解决令人尴尬的并行问题 - 显然,内存访问是限制)。

因此,是时候检查Parallel.For. 在这种情况下,它只是根据工人的数量创建范围 - 每个范围。所以这不是范围 - 没有开销。核心只是运行一个在给定范围内迭代的任务。有一些有趣的地方 - 例如,如果任务时间过长,任务将被“挂起”。但是,它似乎不太适合数据 - 为什么这样的事情会导致与数据大小无关的随机延迟?无论工作多么小,无论工作多么低MaxDegreeOfParallelism,我们都会“随机”减速。这可能是一个问题,但我不知道如何检查它。

最有趣的是,扩展测试数据对异常没有任何影响——虽然它使“好的”并行运行速度更快(甚至在我的测试中接近完美的效率,奇怪的是),“坏”的仍然只是一样糟糕。事实上,在我的几个测试运行的,他们是荒谬坏(最多十次“正常”的循环)。

那么,让我们来看看线程。我人为地增加了线程的数量,ThreadPool以确保扩展线程池不是瓶颈(如果一切正常,应该不会,但是......)。这是第一个惊喜——“好”运行只使用 4-8 个有意义的线程,“坏”运行扩展到池中的所有可用线程,即使有一百个。哎呀?

让我们再次深入研究源代码。Parallel内部用于Task.RunSynchronously运行根分区工作作业,并Wait在结果上使用 s。当我查看并行堆栈时,有 97 个线程在执行循环体,而实际上只有一个线程RunSynchronously在堆栈上(正如预期的那样 - 这是主线程)。其他的是普通线程池线程。任务 ID 还讲述了一个故事——在迭代过程中创建了数千个单独的任务。显然,事情是非常错在这里。即使我删除了整个循环体,这仍然会发生,所以这也不是一些闭包的怪异。

显式设置MaxDegreeOfParallelism在某种程度上抵消了这一点 - 使用的线程数量不再爆炸 - 然而,任务数量仍然如此。但是我们已经看到范围只是运行的并行任务的数量 - 那么为什么要继续创建越来越多的任务呢?使用调试器证实了这一点 - MaxDOP 为 4,只有五个范围(有一些对齐导致了第五个范围)。有趣的是,其中一个已完成的范围(第一个范围如何比其他范围领先这么多?)的索引高于它迭代的范围——这是因为“调度程序”在最多 16 个切片中分配范围分区。

根任务是自我复制的,因此它不是显式启动四个任务来处理数据,而是等待调度程序复制任务来处理更多数据。这有点难读——我们谈论的是复杂的多线程无锁代码,但似乎总是在比分区范围小得多的切片中分配工作。在我的测试中,切片的最大大小为 16 - 与我正在运行的数百万数据相去甚远。像这样的主体进行 16 次迭代根本没有时间,这可能会导致算法出现许多问题(最大的是基础设施比实际的迭代器主体占用更多的 CPU 工作)。在某些情况下,缓存垃圾可能会进一步影响性能(也许当主体运行时有很多变化时),但大多数情况下,访问是连续的。

TL; DR

不要使用Parallel.ForParallel.ForEach如果你的工作每次迭代很短(毫秒级)。AsParallel或者只是运行单线程迭代很可能会快得多。

稍微长一点的解释:

似乎Parallel.For并且Paraller.ForEach是为您迭代的单个项目需要大量时间来执行的场景而设计的(即每个项目的大量工作,而不是大量项目的少量工作)。当迭代器主体太短时,它们似乎表现不佳。如果您没有在迭代器主体中进行大量工作,请使用AsParallel代替Parallel.*。甜蜜点似乎在每片 150 毫秒以下(每次迭代大约 10 毫秒)。否则,Parallel.*将花费大量时间在自己的代码上,几乎没有时间进行迭代(在我的情况下,通常的数字在体内的 5-10% 左右 - 非常糟糕)。

可悲的是,我在 MSDN 上没有发现任何关于此的警告——甚至有样本处理了大量数据,但没有暗示这样做会带来可怕的性能损失。在我的计算机上测试完全相同的示例代码,我发现它确实通常比单线程迭代慢,而且在最好的情况下,几乎没有更快(在四个 CPU 内核上运行时节省大约 30-40% 的时间- 效率不高)。

编辑:

Willaien 在 MSDN 上发现了一个关于这个问题的提及,以及如何解决它 - https://msdn.microsoft.com/en-us/library/dd560853(v=vs.110).aspx。这个想法是使用自定义分区器并在Parallel.For主体中迭代它(例如 loop inParallel.For的循环)。然而,在大多数情况下,使用AsParallel可能仍然是一个更好的选择——简单的循环体通常意味着某种映射/减少操作,AsParallel而 LINQ 通常在这方面做得很好。例如,您的示例代码可以简单地重写为:

var result = testData.AsParallel().Where(i => i == 1).ToList();
Run Code Online (Sandbox Code Playgroud)

使用AsParallel是一个坏主意的唯一情况与所有其他 LINQ 相同 - 当您的循环体有副作用时。有些可能是可以容忍的,但完全避免它们更安全。


wil*_*ien 1

经过一些分析,您可能甚至不会添加到这些集合中:100,000,000 个元素仍然比关键搜索空间(大约 21 亿)小很多,因此这些集合可能不会添加任何元素,或者只添加一两个元素。

至于特定问题,虽然我能够复制它,但我无法直接回答为什么会发生这种情况,但是,我怀疑这在某种程度上与内存总线周围的严重争用有关,以及它如何处理分区和线程创建。将线程数量限制为当前处理器数量似乎有所帮助,但是并不能完全解决问题。

尽管如此,PLINQ 版本似乎更快、更一致:

var resultData = testData.AsParallel().Where(x => x == 1).ToList();
Run Code Online (Sandbox Code Playgroud)

编辑: 看起来这是一个半模糊但已知的问题,更多详细信息可在此处找到:https://msdn.microsoft.com/en-us/library/dd560853 (v=vs.110).aspx