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

时间:2015-08-17 01:33:48

标签: c# parallel-processing cpu-usage

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

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

     <cc1:AsyncFileUpload runat="server" ThrobberID="Throbber" 
                OnUploadedComplete="AsyncFileUpload1_UploadedComplete" 
                OnClientUploadComplete="uploadComplete" OnClientUploadStarted="uploadStarted"

        ID="AsyncFileUpload1" Width="400px"

        CompleteBackColor = ""
        ClientIDMode="AutoID"
        UploadingBackColor=""  

       CssClass="btn btn-warning" ErrorBackColor=""

      />

正常输出

Normal ForEach 493

315并行列表

列出并行ForEach 328

并行并行286

并行并行ForEach 292

在100%CPU使用期间

正常ForEach 476

8047并行列表

List Parallel ForEach 276

并行并行281

Concurrent Parallel ForEach 3960

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

更新

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

            // 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);

3 个答案:

答案 0 :(得分:3)

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

现在,在这样的问题中尝试的第一件事就是使用分析器。所以我做到了。结果如下:

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

第一种方式我们可以说GC可能不是罪魁祸首。实际上,它没有任何机会跑。第二个更好奇 - 这意味着在某些情况下,Parallel的开销是不合时宜的 - 但它看起来是随机的,有时它可以毫无障碍地工作,有时需要半秒钟。这通常指向GC,但我们已经排除了这一点。

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

因此,现在是时候检查Parallel.For的参考来源了。在这种情况下,它只是根据工人数量创建范围 - 每个范围一个范围。所以它不是范围 - 没有开销。  核心只是运行一个迭代给定范围的任务。有一些有趣的东西 - 例如,任务将被暂停&#34;如果需要太长时间。但是,它似乎不太适合数据 - 为什么这样的事情会导致与数据大小无关的随机延迟?无论工作量有多小,无论MaxDegreeOfParallelism有多低,我们都可以随机获得&#34;减速。这可能是一个问题,但我不知道如何检查它。

最有趣的是,扩展测试数据不会对异常产生任何影响 - 同时它会使&#34;良好&#34;并行运行得更快(甚至在我的测试中接近完美的效率,奇怪的是),&#34;坏&#34;那些仍然同样糟糕。事实上,在我的一些测试运行中,它们荒谬不好(高达#34;正常&#34;循环的十倍)。

所以,让我们来看看这些主题。我巧妙地增加了ThreadPool中的线程数量,以确保扩展线程池不是一个瓶颈(如果一切运行良好,它不应该......)。这是第一个惊喜 - 而#34;好&#34;运行只需使用4-8个有意义的线程,&#34;坏&#34; run会扩展池中的所有可用线程,即使它们中有一百个也是如此。糟糕?

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

明确地设置MaxDegreeOfParallelism有点偏移 - 使用的线程数量不再爆炸 - 但是,任务量仍然有效。但我们已经看到范围只是并行任务的运行量 - 那么为什么要继续创建越来越多的任务呢?使用调试器确认这一点 - MaxDOP为4,只有五个范围(有一些对齐导致第五个范围)。有趣的是,其中一个已完成的范围(第一个如何在其余范围之前完成?)的索引高于其迭代的范围 - 这是因为&#34;调度程序&#34;在最多16个切片中分配范围分区。

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

<强> TL; DR

如果您的每次迭代工作非常短(大约为毫秒),请不要使用Parallel.ForParallel.ForEachAsParallel或者只运行迭代单线程很可能会更快。

稍微长一点的解释:

似乎Parallel.ForParaller.ForEach是针对您重复使用的各个项目花费大量时间执行的情况而设计的(即每个项目需要大量工作,而不是很小很多项目的工作量)。当迭代器体太短时,它们似乎表现不佳。如果您未在迭代器主体中执行大量工作,请使用AsParallel代替Parallel.*。甜点似乎在每片150ms以下(每次迭代大约10ms)。否则,Parallel.*将花费大量时间在自己的代码中,并且几乎没有时间进行迭代(在我的情况下,通常的数字在体内约为5-10% - 非常糟糕)。

可悲的是,我在MSDN上没有发现任何有关此问题的警告 - 甚至样本都会查看大量数据,但是没有暗示这样做的可怕性能损失。在我的计算机上测试相同的示例代码,我发现它确实经常比单线程迭代慢,并且在最好的时候,几乎没有更快(大约30-40%的时间节省在四个CPU核心上运行 - 效率不高。)

修改

Willaien在MSDN上发现了关于这个问题的提及,以及如何解决它 - https://msdn.microsoft.com/en-us/library/dd560853(v=vs.110).aspx。我们的想法是使用自定义分区程序并在Parallel.For正文中迭代它(例如Parallel.For循环中的循环)。但是,对于大多数情况,使用AsParallel可能仍然是一个更好的选择 - 简单的循环体通常意味着某种map / reduce操作,AsParallel和LINQ通常都很棒。例如,您的示例代码可以简单地重写为:

var result = testData.AsParallel().Where(i => i == 1).ToList();

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

答案 1 :(得分:1)

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

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

所有这一切,PLINQ版本的东西似乎更快,更一致:

var resultData = testData.AsParallel().Where(x => x == 1).ToList();

修改 看起来这是一个半模糊但已知的问题,更多细节可以在这里找到:https://msdn.microsoft.com/en-us/library/dd560853(v=vs.110).aspx

答案 2 :(得分:0)

我有类似的问题。我正在使用具有16GB RAM的八核I5处理器Parallel.Foreach在进行少量代码更改后将CPU利用率降至20%以下,从而使CPU达到了100%。这是我的示例代码。

static void Main(string[] args)
    {
        List<int> values = Enumerable.Range(1, 100000000).ToList();
        long sum = 0;

        Parallel.ForEach(values,
            new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount },
            x =>
            AddValues(x, ref sum)
            );
        Console.WriteLine(sum);
    }

    private static long AddValues(int x, ref long sum)
    {
        PerformanceCounter cpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total");
        CheckCPUUsageAndSleepThread(cpuCounter);

        int y = x * 5;            
        for (int i=0;i<y;i++)
        {
            CheckCPUUsageAndSleepThread(cpuCounter);
            //do nothing
        }
        return Interlocked.Add(ref sum, x);
    }

    private static void CheckCPUUsageAndSleepThread(PerformanceCounter cpuCounter)
    {
        if (cpuCounter.NextValue() > 80) //Check if CPU utilization crosses 80%  
        {
            Thread.Sleep(1);
        }
    }

当CPU利用率超过80%时,我会暂停1毫秒。这解决了我的问题。如果遇到需要使用parallel.foreach循环的情况,可以尝试调用此函数,否则可以尝试上述解决方案

CheckCPUUsageAndSleepThread()

我希望这会有所帮助。

PS:模拟100%CPU使用率注释 Thread.Sleep(1)