任务并行不稳定,有时使用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, 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);
正常输出
正常ForEach 493
并列315名
列出并行ForEach 328
并行并行286
并行并行ForEach 292
在100%的CPU使用率
正常ForEach 476
列表并行8047
列表并行ForEach 276
并行为281
并行并行ForEach 3960
(这可能发生在任何并行任务期间,以上只是一个实例)
更新
通过使用@willaien提供的PLINQ方法并运行100次,此问题不再发生。 我仍然不知道为什么这个问题首先出现。
var resultData3 = testData.AsParallel().Where(x => x == 1).ToList();
首先,谨慎使用Parallel
- 它并不能保护您免于线程安全问题。 在您的原始代码中,填充结果列表时使用了非线程安全的代码。 一般来说,你想避免共享任何状态(尽管在这种情况下对列表的只读访问是正确的)。 如果你真的想使用Parallel.For
或者Parallel.ForEach
进行过滤和聚合(实际上, AsParallel
是你在这种情况下所需要的),你应该使用线程本地状态的重载 - 你可以做最后的结果聚合localFinally
委托(请注意,它仍然在不同的线程上运行,因此您需要确保线程安全;但是,在这种情况下,锁定是很好的,因为您只在每个线程执行一次,而不是在每次迭代中执行一次)。
现在,尝试解决这个问题的第一件事就是使用一个分析器。 所以我已经做到了。 结果如下:
Parallel.For
或Parallel.ForEach
本体中,而不是在你的代码中(简单的if (data[i] == 1) results.Add(data[i])
)。 我们可以说GC的第一种手段可能不是罪魁祸首。 事实上,它没有任何运行的机会。 第二个更好奇 - 这意味着在某些情况下, Parallel
的开销是Parallel
的 - 但它看起来是随机的,有时它运行顺畅,有时需要半秒钟。 这通常指向GC,但我们已经排除了这一点。
我试过使用没有循环状态的重载,但是没有帮助。 我试图限制MaxDegreeOfParallelism
,但它只会伤害事情。 现在,很明显,这段代码完全由缓存访问控制 - 几乎没有任何CPU工作,也没有I / O - 这将永远支持单线程解决方案; 但即使使用1的MaxDegreeOfParallelism
也无济于事 - 事实上,2似乎是我系统中速度最快的。 更多是无用的 - 同样,缓存访问占主导地位。 它仍然很好奇 - 我正在使用服务器CPU进行测试,它同时拥有大量的所有数据的缓存,虽然我们没有进行100%的顺序访问(几乎完全摆脱了延迟),它应该足够顺序。 无论如何,我们在单线程解决方案中拥有内存吞吐量的基准线,并且它在并行处理的情况下非常接近并行处理速度(并行化,我比单线程读取的运行时间少40%四核服务器CPU的并行问题 - 显然,内存访问是限制)。
因此,是时候检查Parallel.For
的参考源了。 在这种情况下,它只是根据工人的数量创建范围 - 每个范围一个范围。 所以这不是范围 - 从此没有任何开销。 核心只是运行一个在给定范围内迭代的任务。 有一些有趣的地方 - 例如,如果任务花费太长时间,任务将被“暂停”。 但是,它似乎不太适合数据 - 为什么会这样造成与数据大小无关的随机延迟? 无论工作工作多么MaxDegreeOfParallelism
,无论MaxDegreeOfParallelism
多低,我们都会“随机”减速。 这可能是一个问题,但我不知道如何检查它。
最有趣的是,扩展测试数据对异常没有任何影响 - 虽然它使得“良好”并行运行速度更快(即使在我的测试中接近完美效率,足够奇怪),但“不好”的仍然只是一样糟糕。 事实上,在我的一些测试中,它们非常糟糕(高达“正常”循环的十倍)。
所以,让我们看看这些线程。 我人为地在ThreadPool
碰到了线程数量,以确保扩展线程池不是瓶颈(如果一切正常,但不应该......)。 第一个惊喜就是 - “好”运行只需使用4-8个有意义的线程,“坏”运行扩展到池中所有可用的线程,即使有一百个线程也是如此。 哎呀?
让我们再次深入源代码。 Parallel
内部使用Task.RunSynchronously
来运行根分区的工作作业,并Wait
结果。 当我查看并行堆栈时,有97个线程执行循环体,并且只有一个实际上在堆栈上具有RunSynchronously
(如预期的那样 - 这是主线程)。 其他人是普通的线程池线程。 任务ID也说明了一个故事 - 在迭代过程中创建了数千个单独的任务。 很显然,这里有些错误。 即使我删除整个循环体,这仍然会发生,所以它不是一些封闭的怪异。
显式设置MaxDegreeOfParallelism
有点偏移 - 所使用的线程数量不再爆炸 - 但是,任务数量仍然存在。 但我们已经看到,范围只是运行的并行任务的数量 - 为什么要继续创建更多的任务? 使用调试器证实了这一点 - MaxDOP为4,只有5个范围(有一些对齐导致第五个范围)。 有趣的是,其中一个已完成的范围(第一个完成的范围如何超过其他范围?)的索引高于迭代的范围 - 这是因为“调度程序”分配的范围分区最多为16个分区。
根任务是自我复制的,因此不用明确地启动例如四个任务来处理数据,而是等待调度器复制任务以处理更多数据。 这很难阅读 - 我们正在讨论复杂的多线程无锁代码,但它似乎总是分配比分区范围小得多的分片。 在我的测试中,切片的最大尺寸为16,与我运行的数百万条数据相差甚远。 对这样的机构进行16次迭代根本没有时间,这可能会导致算法出现很多问题(最大的原因是基础结构比实际的迭代器主体需要更多的CPU工作)。 在某些情况下,高速缓存垃圾可能会进一步影响性能(可能在正文运行时存在很多变化时),但大多数情况下,访问足够顺序。
TL; DR
如果您的每次迭代工作非常短(毫秒级),请勿使用Parallel.For
和Parallel.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
体中迭代它(例如在Parallel.For
循环中的循环)。 然而,在大多数情况下,使用AsParallel
可能仍然是一个更好的选择 - 简单的循环体通常意味着某种映射/减少操作,而AsParallel
和LINQ通常都是非常棒的。 例如,您的示例代码可以重写为:
var result = testData.AsParallel().Where(i => i == 1).ToList();
唯一的情况是使用AsParallel
是一个坏主意,与所有其他LINQ相同 - 当你的循环体有副作用时。 有些可能是可以忍受的,但完全避免它们更安全。
经过一些分析,你甚至可能不会加入到这些集合中: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
链接地址: http://www.djcxy.com/p/28415.html上一篇: Task Parallel is unstable, using 100% CPU at times
下一篇: input[type="file"] check existence of file attachment with css