C# Parallel.ForEach:当集合的记录计数很高时保存集合的最佳方法?

C# Parallel.ForEach:当集合的记录计数很高时保存集合的最佳方法?,c#,locking,task-parallel-library,parallel.foreach,C#,Locking,Task Parallel Library,Parallel.foreach,所以我运行了一个Parallel.ForEach,它基本上生成了一组数据,这些数据最终将被保存到数据库中。然而,由于数据的收集可能会变得相当大,我需要能够偶尔保存/清除收集,以免遇到OutOfMemoryException 我对使用Parallel.ForEach、并发集合和锁不太熟悉,因此我不太清楚到底需要做些什么来确保一切正常工作(即,在保存和清除操作之间,我们没有向集合添加任何记录) 目前我的意思是,如果记录计数高于某个阈值,请将数据保存在当前集合中的锁块中 ConcurrentStack

所以我运行了一个Parallel.ForEach,它基本上生成了一组数据,这些数据最终将被保存到数据库中。然而,由于数据的收集可能会变得相当大,我需要能够偶尔保存/清除收集,以免遇到
OutOfMemoryException

我对使用
Parallel.ForEach
、并发集合和锁不太熟悉,因此我不太清楚到底需要做些什么来确保一切正常工作(即,在保存和清除操作之间,我们没有向集合添加任何记录)

目前我的意思是,如果记录计数高于某个阈值,请将数据保存在当前集合中的
块中

ConcurrentStack<OutRecord> OutRecs = new ConcurrentStack<OutRecord>();
object StackLock = new object();

Parallel.ForEach(inputrecords, input =>
{
  lock(StackLock)
  {  
    if (OutRecs.Count >= 50000)
    {
       Save(OutRecs);
       OutRecs.Clear();
     }
   }

  OutRecs.Push(CreateOutputRecord(input);
});

if (OutRecs.Count > 0) Save(OutRecs);
ConcurrentStack OutRecs=new ConcurrentStack();
对象堆栈锁=新对象();
Parallel.ForEach(inputrecords,input=>
{
锁(堆叠锁)
{  
如果(OutRecs.Count>=50000)
{
保存(OutRecs);
OutRecs.Clear();
}
}
OutRecs.Push(CreateOutputRecord(输入);
});
如果(OutRecs.Count>0)保存(OutRecs);

我不能100%确定这是否像我认为的那样工作。锁是否阻止循环的其他实例写入输出集合?如果没有,是否有更好的方法来执行此操作?

您的锁将正常工作,但效率不高,因为您的所有工作线程都将在此外,锁往往(相对)昂贵,因此在每个线程的每次迭代中执行锁有点浪费

您的一条评论提到为每个工作线程提供自己的数据存储:是的,您可以这样做。下面是一个您可以根据需要定制的示例:

Parallel.ForEach(
    // collection of objects to iterate over
    inputrecords,

    // delegate to initialize thread-local data
    () => new List<OutRecord>(),

    // body of loop
    (inputrecord, loopstate, localstorage) =>
    {
        localstorage.Add(CreateOutputRecord(inputrecord));
        if (localstorage.Count > 1000)
        {
            // Save() must be thread-safe, or you'll need to wrap it in a lock
            Save(localstorage);
            localstorage.Clear();
        }
        return localstorage;
    },

    // finally block gets executed after each thread exits
    localstorage =>
    {
        if (localstorage.Count > 0)
        {
            // Save() must be thread-safe, or you'll need to wrap it in a lock
            Save(localstorage);
            localstorage.Clear();
        }
    });
Parallel.ForEach(
//要迭代的对象集合
输入记录,
//委托初始化线程本地数据
()=>新列表(),
//环体
(inputrecord、loopstate、localstorage)=>
{
Add(CreateOutputRecord(inputrecord));
如果(localstorage.Count>1000)
{
//Save()必须是线程安全的,否则需要将其封装在锁中
保存(本地存储);
localstorage.Clear();
}
返回本地存储;
},
//最后,在每个线程退出后执行块
本地存储=>
{
如果(localstorage.Count>0)
{
//Save()必须是线程安全的,否则需要将其封装在锁中
保存(本地存储);
localstorage.Clear();
}
});

一种方法是定义一个表示数据目的地的抽象。它可以是这样的:

public interface IRecordWriter<T> // perhaps come up with a better name.
{
    void WriteRecord(T record);
    void Flush();
}
public abstract class BufferedRecordWriter<T> : IRecordWriter<T>
{
    private readonly ConcurrentQueue<T> _buffer = new ConcurrentQueue<T>();
    private readonly int _maxCapacity;
    private bool _flushing;

    public ConcurrentQueueRecordOutput(int maxCapacity = 100)
    {
        _maxCapacity = maxCapacity;
    }

    public void WriteRecord(T record)
    {
        _buffer.Enqueue(record);
        if (_buffer.Count >= _maxCapacity && !_flushing)
            Flush();
    }

    public void Flush()
    {
        _flushing = true;
        try
        {
            var recordsToWrite = new List<T>();
            while (_buffer.TryDequeue(out T dequeued))
            {
                recordsToWrite.Add(dequeued);
            }
            if(recordsToWrite.Any())
                WriteRecords(recordsToWrite);
        }
        finally
        {
            _flushing = false;
        }
    }

    protected abstract void WriteRecords(IEnumerable<T> records);
}
当缓冲区达到最大大小时,缓冲区中的所有记录都会发送到
WriteRecords
。因为
\u buffer
是一个
ConcurrentQueue
它可以在添加记录时保持读取记录

Flush
方法可以是任何特定于您如何编写记录的方法。它不是一个抽象类,而是数据库或文件的实际输出可能是另一个注入到其中的依赖项。您可以做出这样的决策、重构和改变主意,因为第一个类不受这些变化。它只知道
IRecordWriter
界面没有变化

您可能会注意到,我还没有完全确定
Flush
不会在不同的线程上并发执行。我可以在这一点上设置更多的锁定,但这并不重要。这将避免大多数并发执行,但如果并发执行都从
ConcurrentQueue
读取,则没有关系


这只是一个粗略的概述,但它显示了如果我们将它们分开,所有的步骤是如何变得更简单和更容易测试的。一个类将输入转换为输出。另一个类缓冲并写入输出。第二个类甚至可以分为两个-一个作为缓冲,另一个作为“最终”将它们发送到数据库、文件或其他目的地的写入程序。

让每个线程保存自己的数据怎么样?在事务量很大的情况下,逐个保存记录可能会非常慢,特别是当涉及数据库时。啊,我绝对不想保存每个迭代。如果每个线程都有某种方法可以保存它们自己的数据的话他希望有自己的收藏,一旦达到一定数量就可以保存,然后一旦完成就可以保存剩下的东西——也许这会起作用。不确定是否有办法做到这一点,实际上只是发现了并行。ForEach,所以我仍在努力弄清楚。