C#使用TPL Parallel ForEach将List <t>拆分为组

时间:2016-12-06 09:50:00

标签: c# split parallel-processing thread-safety task-parallel-library

我需要处理数千个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日

我更新了代码以满足验证要求。

1 个答案:

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

但是,我想注意您有一些小问题:

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

    这有助于您避免不必要的分组数据副本

  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;
    }
    

    lambda已正确编译,因为您已经有一个泛型集合(一个包),并且有一个重载接受ParallelLoopState作为第二个参数。