我需要处理数千个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
,但我从未使用过它,无论如何它现在不是我的首要任务。
我也已经读过这个类似的question但是没有解决我在内循环中遇到的问题。
更新2016年12月28日
我更新了代码以满足验证要求。
答案 0 :(得分:2)
代码更新后,您似乎正在使用ConcurrentBag
,因此代码中还有另一个非线程安全逻辑:
long currentPackageSize = 0;
if (// .. &&
currentPackageSize + currentDocumentSize > Parameters.MaxPackageSize
// ...
{
// ...
currentPackageSize += currentDocumentSize;
}
+=
运算符不是原子的,你肯定会遇到竞争条件,并且读取long
变量的值在这里不是线程安全的。您可以在那里介绍locks
,或使用Interlocked
class以原子方式更新值:
Interlocked.Add(ref currentPackageSize, currentDocumentSize);
Interlocked.Exchange(ref currentPackageSize, 0);
Interlocked.Read(ref currentPackageSize);
使用这个类将导致一些重构代码(我认为CAS
操作的使用,例如CompareExchange
更适合你的情况),所以,也许对你来说这是最简单的方法使用锁。您可能应该实现两种方法并测试它们并测量执行时间。
另外,正如您所看到的,实例化也不是线程安全的,因此您必须锁定变量(这将导致线程同步暂停)或将代码重构为两步:首先你可以并行获得所有文件大小,然后以顺序方式迭代结果,避免竞争条件。
至于Partitioner
,你不应该在这里使用这个类,因为它通常用于在CPU上安排工作,而不是分割结果。
但是,我想注意您有一些小问题:
您可以在ToList()
的构造函数中删除ConcurrentBag
次调用,因为它接受您已经拥有的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;
}
lambda已正确编译,因为您已经有一个泛型集合(一个包),并且有一个重载接受ParallelLoopState
作为第二个参数。