c# – 任务并行不稳定,有时使用100%CPU
我目前正在测试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,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,loopState) => { if (item.Equals(1)) { resultData2.Push(item); } }); Console.WriteLine("Concurrent Parallel ForEach " + sw.ElapsedMilliseconds); 正常输出 正常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(); 解决方法
首先,小心并行 – 它不会保护您免受线程安全问题的影响.在原始代码中,在填充结果列表时使用了非线程安全代码.通常,您希望避免共享任何状态(尽管在这种情况下对列表的只读访问权限很好).如果你真的想使用Parallel.For或Parallel.ForEach进行过滤和聚合(实际上,AsParallel就是你想要的那些情况),你应该使用带有线程局部状态的重载 – 你要做最后的结果聚合localFinally委托(请注意,它仍然在不同的线程上运行,因此您需要确保线程安全;但是,在这种情况下,锁定很好,因为每个线程只执行一次,而不是每次迭代).
现在,在这样的问题中尝试的第一件事就是使用分析器.所以我做到了.结果如下: >这些解决方案中几乎没有任何内存分配.它们与初始测试数据分配完全相形见绌,即使对于相对较小的测试数据(我在测试时使用了1M,10M和100M的整数). 第一种方式我们可以说GC可能不是罪魁祸首.实际上,它没有任何机会运行.第二个更好奇 – 这意味着在某些情况下,Parallel的开销是不合时宜的 – 但它看起来是随机的,有时它可以毫无障碍地工作,有时需要半秒钟.这通常指向GC,但我们已经排除了这一点. 我已经尝试使用没有循环状态的重载,但这没有帮助.我试过限制MaxDegreeOfParallelism,但它只会伤害事物.现在,很明显,这个代码绝对由高速缓存访??问控制 – 几乎没有CPU工作和没有I / O – 这总是有利于单线程解决方案;但即使使用1的MaxDegreeOfParallelism也无济于事 – 事实上,2似乎是我系统中最快的.更多是无用的 – 再次,缓存访问占主导地位.它仍然很奇怪 – 我正在使用服务器CPU进行测试,它同时为所有数据提供了大量缓存,而我们没有进行100%顺序访问(这几乎完全消除了延迟) ),应该足够顺序.无论如何,我们在单线程解决方案中拥有内存吞吐量的基线,并且当它运行良好时非常接近并行化案例的速度(并行化,我读取的运行时间比单线程少40%)四核服务器CPU用于一个令人尴尬的并行问题 – 显然,内存访问是极限. 因此,是时候检查Parallel.For的参考源了.在这种情况下,它只是根据工人数量创建范围 – 每个范围一个范围.所以这不是范围 – 没有开销. 最有趣的是扩展测试数据与异常无关 – 虽然它使“好”并行运行得更快(甚至在我的测试中接近完美效率,奇怪的是),“坏”仍然只是一样糟糕.事实上,在我的一些测试中,它们非常糟糕(高达“正常”循环的十倍). 那么,让我们来看看线程.我巧妙地增加了ThreadPool中的线程数量,以确保扩展线程池不是瓶颈(如果一切运行良好,它不应该……).这是第一个惊喜 – 虽然“好”运行只使用4-8个有意义的线程,但“坏”运行会扩展到池中所有可用线程,即使它们有一百个.哎呀? 让我们再次深入研究源代码. Parallel内部使用Task.RunSynchronously运行根分区工作作业,并等待结果.当我查看并行堆栈时,有97个线程执行循环体,并且只有一个实际上在堆栈上具有RunSynchronously(正如预期的那样 – 这是主线程).其他是普通的线程池线程.任务ID也讲述了一个故事 – 在进行迭代时会创建数千个单独的任务.显然,这里有些不对劲.即使我移除整个循环体,这仍然会发生,所以它也不是一些封闭的怪异. 显式设置MaxDegreeOfParallelism有点抵消 – 使用的线程数量不会再爆炸 – 但是,任务量仍然有效.但是我们已经看到范围只是运行并行任务的数量 – 那么为什么要继续创建越来越多的任务呢?使用调试器确认这一点 – MaxDOP为4,只有五个范围(有一些对齐导致第五个范围).有趣的是,其中一个已完成的范围(第一个如何在其余范围之前完成?)的索引高于它迭代的范围 – 这是因为“调度程序”在最多16个切片中分配范围分区. 根任务是自我复制的,因此不是明确地启动,例如处理数据的四个任务,它等待调度程序复制任务以处理更多数据.这有点难以阅读 – 我们谈论的是复杂的多线程无锁代码,但它似乎总是将分配范围内的工作分配得比分区范围小得多.在我的测试中,切片的最大尺寸为16 – 与我正在运行的数百万个数据相差甚远.像这样的16次迭代根本没有时间,这可能会导致算法出现许多问题(最大的问题是基础设施占用的CPU工作量比实际的迭代器主体多).在某些情况下,缓存垃圾可能会进一步影响性能(可能在正文运行时存在很多变化时),但大多数情况下,访问是连续的. TL; DR 如果您的每次迭代工作非常短(大约几毫秒),请不要使用Parallel.For和Parallel.ForEach. AsParallel或只运行迭代单线程将更快. 稍微长一点的解释: 似乎Parallel.For和Paraller.ForEach是针对你正在迭代的各个项目需要花费大量时间来执行的场景(即每个项目的大量工作,而不是很多项目的大量工作) .当迭代器体太短时,它们似乎表现不佳.如果您没有在迭代器主体中进行大量工作,请使用AsParallel而不是Parallel.*.甜点似乎在每片150ms以下(每次迭代大约10ms).否则,Parallel.*将花费大量时间在自己的代码中,并且几乎没有时间进行迭代(在我的情况下,通常的数字在体内约为5-10% – 非常糟糕). 可悲的是,我没有在MSDN上发现任何有关此问题的警告 – 甚至有大量数据的样本,但没有暗示这样做的可怕性能损失.在我的计算机上测试相同的示例代码,我发现它确实经常比单线程迭代慢,并且在最好的时候,几乎更快(在四个CPU内核上运行时节省大约30-40%的时间) – 不是很有效率). 编辑: 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相同 – 当你的循环体有副作用时.有些可能是可以忍受的,但完全避免它们会更安全. (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |