我有一些代码,用自己类似R的C# DataFrame类来处理数百万数据行。其中有许多Parallel.ForEach调用,以并行迭代数据行。此代码已使用VS2013和.NET 4.5运行了一年多,没有问题。
我有两台开发机器(A和B),最近将A机升级到了VS2015。我开始注意到我的代码会时不时奇怪地冻结。长时间运行后,发现代码确实最终完成。只是需要15-120分钟而不是1-2分钟。
尝试在VS2015调试器中使用Break All功能失败了。因此,我插入了一堆日志语句。结果发现,在Parallel.ForEach循环期间进行Gen2收集(比较每个Parallel.ForEach循环之前和之后的收集计数)时会出现这种冻结。如果有任何一个Parallel.ForEach循环调用与Gen2收集重合(如果有的话),额外的13-118分钟都会花在里面。如果在任何Parallel.ForEach循环期间没有Gen2收集(我运行它时大约有50%的时间),那么一切都会在1-2分钟内完成。
当我在A机上使用VS2013运行相同的代码时,我遇到了相同的冻结。当我在Machine B上(未经升级的机器)运行代码时,完全正常。它在一夜之间运行了数十次而没有冻结。
我注意到/尝试过的一些事情:
- 在A机上附加或不附加调试器时都会冻结(起初我认为是与VS2015调试器有关)
- 无论我以Debug模式还是Release模式构建,都会发生冻结
- 如果我目标定位为.NET 4.5或.NET 4.6,也会发生冻结
- 我尝试禁用RyuJIT,但这并未影响冻结
我并没有改变默认的GC设置。根据GCSettings,所有运行都使用LatencyMode Interactive和IsServerGC为false。
我可以在每次调用Parallel.ForEach之前切换到LowLatency,但我真的希望了解正在发生的事情。
在VS2015升级后,有人看到Parallel.ForEach中的奇怪冻结吗?下一步该做什么?
更新1:添加一些样例代码来解释上面的含糊不清的内容...
以下是一些示例代码,我希望能演示此问题。B机上的代码在10-12秒内稳定运行。它遇到了许多Gen2收集,但几乎不需要时间。如果我取消注释两个GC设置行,我可以强制它没有Gen2收集。此时速度稍慢,大约为30-50秒。
现在,在我的A机上,代码需要随机的时间。似乎在5到30分钟之间。如果遇到越来越多的Gen2收集,情况似乎会变得更糟。如果我取消注释两个GC设置行,A机上也需要30-50秒(与B机器相同)。
这可能需要对行数和数组大小进行一些调整,才能在另一台机器上显示出来。
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Linq;
using System.Runtime;
public class MyDataRow
{
public int Id { get; set; }
public double Value { get; set; }
public double DerivedValuesSum { get; set; }
public double[] DerivedValues { get; set; }
}
class Program
{
static void Example()
{
const int numRows = 2000000;
const int tempArraySize = 250;
var r = new Random();
var dataFrame = new List<MyDataRow>(numRows);
for (int i = 0; i < numRows; i++) dataFrame.Add(new MyDataRow { Id = i, Value = r.NextDouble() });
Stopwatch stw = Stopwatch.StartNew();
int gcs0Initial = GC.CollectionCount(0);
int gcs1Initial = GC.CollectionCount(1);
int gcs2Initial = GC.CollectionCount(2);
//GCSettings.LatencyMode = GCLatencyMode.LowLatency;
Parallel.ForEach(dataFrame, dr =>
{
double[] tempArray = new double[tempArraySize];
for (int j = 0; j < tempArraySize; j++) tempArray[j] = Math.Pow(dr.Value, j);
dr.DerivedValuesSum = tempArray.Sum();
dr.DerivedValues = tempArray.ToArray();
});
int gcs0Final = GC.CollectionCount(0);
int gcs1Final = GC.CollectionCount(1);
int gcs2Final = GC.CollectionCount(2);
stw.Stop();
//GCSettings.LatencyMode = GCLatencyMode.Interactive;
Console.Out.WriteLine("ElapsedTime = {0} Seconds ({1} Minutes)", stw.Elapsed.TotalSeconds, stw.Elapsed.TotalMinutes);
Console.Out.WriteLine("Gcs0 = {0} = {1} - {2}", gcs0Final - gcs0Initial, gcs0Final, gcs0Initial);
Console.Out.WriteLine("Gcs1 = {0} = {1} - {2}", gcs1Final - gcs1Initial, gcs1Final, gcs1Initial);
Console.Out.WriteLine("Gcs2 = {0} = {1} - {2}", gcs2Final - gcs2Initial, gcs2Final, gcs2Initial);
Console.Out.WriteLine("Press Any Key To Exit...");
Console.In.ReadLine();
}
static void Main(string[] args)
{
Example();
}
}
更新2:为了将事情从评论中挪出来留给未来的读者...
这个热修复程序:https://support.microsoft.com/zh-cn/kb/3088957 完全解决了这个问题。我在应用后没有看到任何缓慢问题。
我认为这与 Parallel.ForEach 没有任何关系,基于这个:http://blogs.msdn.com/b/maoni/archive/2015/08/12/gen2-free-list-changes-in-clr-4-6-gc.aspx,尽管热修复程序的一些说明提到了 Parallel.ForEach。
GC.Collect()
强制进行垃圾回收时会发生什么? - svick