我目前正在测试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
正常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);
答案 0 :(得分:3)
首先,小心Parallel
- 它不会保护您免受线程安全问题的影响。在原始代码中,您在填写结果列表时使用了非线程安全的代码。通常,您希望避免共享任何状态(尽管在这种情况下对列表的只读访问权限很好)。如果你真的想使用Parallel.For
或Parallel.ForEach
进行过滤和聚合(实际上,AsParallel
就是你想要的那些情况),你应该使用带有线程本地状态的重载 - 你和#39; d在localFinally
委托中进行最终结果聚合(请注意,它仍然在不同的线程上运行,因此您需要确保线程安全;但是,在这种情况下,锁定很好,因为你每个线程只做一次,而不是每次迭代都这样做。
现在,在这样的问题中尝试的第一件事就是使用分析器。所以我做到了。结果如下:
Parallel.For
或Parallel.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.For
和Parallel.ForEach
。 AsParallel
或者只运行迭代单线程很可能会更快。
稍微长一点的解释:
似乎Parallel.For
和Paraller.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)