C#拆分列表<;T>;使用TPL并行ForEach分组

C#拆分列表<;T>;使用TPL并行ForEach分组,c#,split,parallel-processing,thread-safety,task-parallel-library,C#,Split,Parallel Processing,Thread Safety,Task Parallel Library,我需要处理一个包含数千个元素的列表 首先,我需要按年份和类型对元素进行分组,以便获得一个列表。然后,对于每个内部列表,我想添加类型为T的对象,直到列表达到最大包大小,然后我创建一个新包,并以相同的方式继续 我想使用并行.ForEach循环。 如果我按顺序运行,我的实际实现工作得很好,但是逻辑不是线程安全的,我想更改它。 我认为问题在于内部并行。ForEach循环,当列表达到最大大小时,我在同一引用中实例化了一个新的列表 private ConcurrentBag<ConcurrentBag

我需要处理一个包含数千个元素的
列表

首先,我需要按年份和类型对元素进行分组,以便获得一个
列表
。然后,对于每个内部
列表
,我想添加类型为T的对象,直到
列表
达到最大包大小,然后我创建一个新包,并以相同的方式继续

我想使用
并行.ForEach
循环。

如果我按顺序运行,我的实际实现工作得很好,但是逻辑不是线程安全的,我想更改它。
我认为问题在于内部
并行。ForEach
循环,当
列表
达到最大大小时,我在同一引用中实例化了一个新的
列表

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; }
}
我到处读到,我也可以使用
分区器,但我从来没有使用过,而且目前这不是我的优先事项

我也读过类似的文章,但没有解决我的内循环问题

更新日期2016年12月28日


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

代码更新后,您似乎正在使用
ConcurrentBag
,因此代码中还剩下另一个非线程安全逻辑:

long currentPackageSize = 0;
if (// .. && 
    currentPackageSize + currentDocumentSize > Parameters.MaxPackageSize
// ...
{
    // ...
    currentPackageSize += currentDocumentSize;
}
foreach (var group in groups)
+=
操作符不是原子的,在这里肯定会有竞争条件,读取
long
变量的值在这里不是线程安全的。您可以在此处引入
,或使用以原子方式更新值:

Interlocked.Add(ref currentPackageSize, currentDocumentSize);
Interlocked.Exchange(ref currentPackageSize, 0);
Interlocked.Read(ref currentPackageSize);
使用这个类将导致一些重构代码(我认为在您的情况下,使用
CAS
操作,如
compareeexchange
更可取),因此,对于您来说,使用锁可能是最简单的方法。您可能应该实现这两种方法并测试它们,并测量执行时间)

此外,正如您所看到的,实例化也不是线程安全的,因此您必须锁定变量(这将导致线程同步暂停)或将代码重构为两个步骤:首先并行获取所有文件大小,然后按顺序迭代结果,避免争用条件

至于
分区器
,您不应该在这里使用此类,因为它通常用于跨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)));
    
  • 除非您知道自己在做什么(我认为您不知道),否则不应该使用最大程度的并行性:

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

  • 您可以对
    Parallel.ForEach
    使用
    lambda
    语法,并且不创建新的
    操作
    (您也可以将此代码移到方法例程):

    (文档、状态)=>
    {
    long currentDocumentSize=新文件信息(document.FilePath).Length;
    //如果MaxPackageSize=0,则无需应用拆分,该过程运行良好
    如果(Parameters.MaxPackageSize>0&¤tPackageSize+currentDocumentSize>Parameters.MaxPackageSize)
    {
    documentGroups.Add(documentGroup);
    //问题来了!
    documentGroup=新的ConcurrentBag();
    currentPackageSize=0;
    }
    documentGroup.Add(文档);
    currentPackageSize+=currentDocumentSize;
    }
    
    lambda被正确编译,因为您已经有了一个通用集合(一个包),并且有一个重载接受
    ParallelLoopState
    作为第二个参数


  • 你只是想通过并行来加速事情吗?在这个特定的例子中,是的。我想加快包的初始化速度。然后每个包(ConcurrentBag)通过一个更复杂的并行ForEach循环来处理文档。在一个线程上处理数据几乎总是比并行处理更快。只有当您有一些繁重的处理时,才值得并行地做任何事情。但是如果我需要在循环中做一些更复杂的事情呢?如何使代码线程安全?这是我的问题…当然。你能修改你的代码,使之成为一个好代码吗?那就可以回答了,谢谢!我最终通过两个步骤重构了代码。但我也很感激你在这门互锁的课上的夸夸其谈。在我的代码中,我在其他一些ParallelForEach块上使用了它,特别是在计数器上。不客气:)是的,对于计数器,这样的增量非常有用。祝你的项目好运。
    foreach (var group in groups)
    
    var options = new ParallelOptions { MaxDegreeOfParallelism = Parameters.MaxDegreeOfParallelism };
    
    (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;
    }