繁体   English   中英

C#拆分列表 <T> 使用TPL Parallel ForEach分组

[英]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上安排工作,而不是分割结果。

但是,我想指出您有一些次要的代码问题:

  1. 您可以在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))); 

    这将帮助您避免不必要的分组数据副本

  2. 您可以使用var关键字来避免代码中类型的重复(这只是示例行,可以在代码中多次更改):

     foreach (var group in groups) 
  3. 除非您知道自己在做什么(并且我认为不是),否则不应该使用最大并行度:

     var options = new ParallelOptions { MaxDegreeOfParallelism = Parameters.MaxDegreeOfParallelism }; 

    TPL默认任务计划程序会尝试调整任务的线程池和CPU使用率,因此通常此数字应等于Environment.ProcessorCount

  4. 您可以将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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM