[英]C# Parallel.Foreach int[] in List<T> not changed properly
[英]C# Split List<T> into groups using TPL Parallel ForEach
我需要处理成千上万个元素的List<T>
。
首先,我需要按年份和类型对元素进行分组,因此获得List<List<T>>
。 然后,对于每个内部List<T>
我想添加类型T的对象,直到List<T>
达到最大包大小,然后创建一个新包,并以相同的方式进行。
我想使用Parallel.ForEach
循环。
如果我按顺序运行它,我的实际实现效果很好,但是逻辑不是线程安全的,我想更改它。
我认为问题出在内部Parallel.ForEach
循环上,当达到List<T>
的最大大小时,我在同一引用内实例化了一个新的List<T>
。
private ConcurrentBag<ConcurrentBag<DumpDocument>> InitializePackages()
{
// Group by Type and Year
ConcurrentBag<ConcurrentBag<DumpDocument>> groups = new ConcurrentBag<ConcurrentBag<DumpDocument>>(Dump.DumpDocuments.GroupBy(d => new { d.Type, d.Year })
.Select(g => new ConcurrentBag<DumpDocument> (g.ToList()))
.ToList());
// Documents lists with max package dimension
ConcurrentBag<ConcurrentBag<DumpDocument>> documentGroups = new ConcurrentBag<ConcurrentBag<DumpDocument>>();
foreach (ConcurrentBag<DumpDocument> group in groups)
{
long currentPackageSize = 0;
ConcurrentBag<DumpDocument> documentGroup = new ConcurrentBag<DumpDocument>();
ParallelOptions options = new ParallelOptions { MaxDegreeOfParallelism = Parameters.MaxDegreeOfParallelism };
Parallel.ForEach(group, options, new Action<DumpDocument, ParallelLoopState>((DumpDocument document, ParallelLoopState state) =>
{
long currentDocumentSize = new FileInfo(document.FilePath).Length;
// If MaxPackageSize = 0 then no splitting to apply and the process works well
if (Parameters.MaxPackageSize > 0 && currentPackageSize + currentDocumentSize > Parameters.MaxPackageSize)
{
documentGroups.Add(documentGroup);
// Here's the problem!
documentGroup = new ConcurrentBag<DumpDocument>();
currentPackageSize = 0;
}
documentGroup.Add(document);
currentPackageSize += currentDocumentSize;
}));
if (documentGroup.Count > 0)
documentGroups.Add(documentGroup);
}
return documentGroups;
}
public class DumpDocument
{
public string Id { get; set; }
public long Type { get; set; }
public string MimeType { get; set; }
public int Year { get; set; }
public string FilePath { get; set; }
}
由于我的操作非常简单,因此实际上我只需要使用以下方法获取文件大小:
long currentDocumentSize = new FileInfo(document.FilePath).Length;
我了解到我也可以使用Partitioner
,但是我从未使用过它,无论如何现在这不是我的优先考虑。
我也已经读过类似的问题 ,但不能解决内部循环问题。
更新28/12/2016
我更新了代码以满足验证要求。
代码更新后,您似乎正在使用ConcurrentBag
因此代码中还有另一个非线程安全的逻辑:
long currentPackageSize = 0;
if (// .. &&
currentPackageSize + currentDocumentSize > Parameters.MaxPackageSize
// ...
{
// ...
currentPackageSize += currentDocumentSize;
}
+=
运算符不是原子的,您肯定在那里有一个竞争条件,在这里读取long
变量的值不是线程安全的。 您可以在此处引入locks
,也可以使用Interlocked
类自动更新该值:
Interlocked.Add(ref currentPackageSize, currentDocumentSize);
Interlocked.Exchange(ref currentPackageSize, 0);
Interlocked.Read(ref currentPackageSize);
使用此类将导致一些重构代码(我认为,在您的情况下,最好使用CAS
操作(例如CompareExchange
)),因此,也许对您而言,这是使用锁的最简单方法。 您可能应该同时实现这两种方法并对其进行测试,并衡量执行时间。
而且,正如您所看到的,实例化也不是线程安全的,因此您必须锁定变量(这将导致线程同步暂停)或将代码重构为两步:首先,您获取了所有文件并行排列大小,然后按顺序迭代结果,避免出现竞争情况。
至于Partitioner
,您不应该在这里使用此类,因为它通常用于在CPU上安排工作,而不是分割结果。
但是,我想指出您有一些次要的代码问题:
您可以在ConcurrentBag
的构造函数中删除ToList()
调用,因为它接受IEnumerable
,您已经拥有它:
ConcurrentBag<ConcurrentBag<DumpDocument>> groups = new ConcurrentBag<ConcurrentBag<DumpDocument>>(Dump.DumpDocuments.GroupBy(d => new { d.Type, d.Year }) .Select(g => new ConcurrentBag<DumpDocument> (g)));
这将帮助您避免不必要的分组数据副本
您可以使用var
关键字来避免代码中类型的重复(这只是示例行,可以在代码中多次更改):
foreach (var group in groups)
除非您知道自己在做什么(并且我认为不是),否则不应该使用最大并行度:
var options = new ParallelOptions { MaxDegreeOfParallelism = Parameters.MaxDegreeOfParallelism };
TPL
默认任务计划程序会尝试调整任务的线程池和CPU使用率,因此通常此数字应等于Environment.ProcessorCount
。
您可以将lambda
语法用于Parallel.ForEach
,而不创建新的Action
(也可以将以下代码移到方法例程中):
(document, state) => { long currentDocumentSize = new FileInfo(document.FilePath).Length; // If MaxPackageSize = 0 then no splitting to apply and the process works well if (Parameters.MaxPackageSize > 0 && currentPackageSize + currentDocumentSize > Parameters.MaxPackageSize) { documentGroups.Add(documentGroup); // Here's the problem! documentGroup = new ConcurrentBag<DumpDocument>(); currentPackageSize = 0; } documentGroup.Add(document); currentPackageSize += currentDocumentSize; }
因为您已经有一个通用集合(一个袋子),并且已经接受了ParallelLoopState
作为第二个参数,所以lambda正确编译。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.