【问题标题】:Thread-safe buffer of data to make batch inserts of controlled size数据的线程安全缓冲区,以控制大小的批量插入
【发布时间】:2011-08-03 14:59:08
【问题描述】:

我有一个模拟生成的数据必须保存到数据库中。

ParallelLoopResult res = Parallel.For(0, 1000000, options, (r, state) =>
{
    ComplexDataSet cds = GenerateData(r);

    SaveDataToDatabase(cds);

});

模拟会生成大量数据,因此先生成然后将其保存到数据库(最多 1 GB 数据)是不切实际的,将其保存到数据库也没有意义一个接一个(交易太小,不实用)。我想将它们作为受控大小的批量插入(比如一次提交 100)插入到数据库中。

但是,我认为我对并行计算的了解并不那么理论化。我想出了这个(你可以看到这是非常有缺陷的):

DataBuffer buffer = new DataBuffer(...);

ParallelLoopResult res = Parallel.For(0, 10000000, options, (r, state) =>
{
    ComplexDataSet cds = GenerateData(r);

    buffer.SaveDataToBuffer(cds, i == r - 1);

});

public class DataBuffer
{
    int count = 0;
    int limit = 100

    object _locker = new object();

    ConcurrentQueue<ConcurrentBag<ComplexDataSet>> ComplexDataBagQueue{ get; set; }

    public void SaveDataToBuffer(ComplexDataSet data, bool isfinalcycle)
    {
            lock (_locker)
            {
                if(count >= limit)
                {
                    ConcurrentBag<ComplexDataSet> dequeueRef;
                    if(ComplexDataBagQueue.TryDequeue(out dequeueRef))
                    {
                        Commit(dequeueRef);
                    }

                    _lastItemRef = new ConcurrentBag<ComplexDataSet>{data};
                    ComplexDataSetsQueue.Enqueue(_lastItemRef);
                    count = 1;
                }
                else
                {
                    // First time
                    if(_lastItemRef == null)
                    {
                        _lastItemRef = new ConcurrentBag<ComplexDataSet>{data};
                        ComplexDataSetsQueue.Enqueue(_lastItemRef);
                        count = 1;
                    }
                    // If buffer isn't full
                    else
                    {
                        _lastItemRef.Add(data);
                        count++;
                    }
                }

                if(isfinalcycle)
                {
                        // Commit everything that hasn't been committed yet
                        ConcurrentBag<ComplexDataSet> dequeueRef;    
                    while (ComplexDataSetsQueue.TryDequeue(out dequeueRef))
                    {
                        Commit(dequeueRef);
                    }
                }
            }
    }

    public void Commit(ConcurrentBag<ComplexDataSet> data)
    {
        // Commit data to database..should this be somehow in another thread or something ?
    }
}

如您所见,我使用队列来创建缓冲区,然后手动决定何时提交。但是,我有一种强烈的感觉,这对我的问题来说并不是很有效的解决方案。首先,我不确定我是否正确锁定。其次,我不确定这是否是完全线程安全的(或根本不安全)。

您能否看一下并评论我应该做些什么不同的事情?或者如果有更好的方法来做到这一点(使用某种生产者-消费者技术或其他东西)?

感谢和最良好的祝愿, D.

【问题讨论】:

  • 您是否已经遇到过某种瓶颈?你说你想要做 100 批增量,但这会给你的数据库连接或其他资源带来额外的瓶颈吗?在开始并行化之前评估实数总是好的做法。
  • @Bryan - 是的,这是我经历过的瓶颈。太多的事务会产生巨大的开销。

标签: c# .net parallel-processing task-parallel-library parallel-extensions


【解决方案1】:

无需使用锁或昂贵的并发安全数据结构。数据都是独立的,因此引入锁定和共享只会损害性能和可扩展性。

Parallel.For 有一个重载,可让您指定每个线程的数据。在此您可以存储私有队列和私有数据库连接。

另外:Parallel.For 在内部将您的范围划分为更小的块。传递一个很大的范围是非常有效的,所以没有什么可以改变的。

Parallel.For(0, 10000000, () => new ThreadState(),
    (i, loopstate, threadstate) =>
{
    ComplexDataSet data = GenerateData(i);

    threadstate.Add(data);

    return threadstate;
}, threadstate => threadstate.Dispose());

sealed class ThreadState : IDisposable
{
    readonly IDisposable db;
    readonly Queue<ComplexDataSet> queue = new Queue<ComplexDataSet>();

    public ThreadState()
    {
        // initialize db with a private MongoDb connection.
    }

    public void Add(ComplexDataSet cds)
    {
        queue.Enqueue(cds);

        if(queue.Count == 100)
        {
            Commit();
        }
    }

    void Commit()
    {
        db.Write(queue);
        queue.Clear();
    }

    public void Dispose()
    {
        try
        {
            if(queue.Count > 0)
            {
                Commit();
            }
        }
        finally
        {
            db.Dispose();
        }
    }
}

现在,MongoDb 目前不支持真正的并发插入——它在服务器中持有一些昂贵的锁,因此并行提交不会为您带来太多(如果有的话)速度。他们希望在未来解决此问题,因此您有一天可能会获得免费加速。

如果您需要限制数据库连接的数量,生产者/消费者设置是一个不错的选择。您可以使用BlockingCollection 队列来高效地执行此操作,而无需使用任何锁:

// Specify a maximum of 1000 items in the collection so that we don't
// run out of memory if we get data faster than we can commit it.
// Add() will wait if it is full.

BlockingCollection<ComplexDataSet> commits =
    new BlockingCollection<ComplexDataSet>(1000);

Task consumer = Task.Factory.StartNew(() =>
    {
        // This is the consumer.  It processes the
        // "commits" queue until it signals completion.

        while(!commits.IsCompleted)
        {
            ComplexDataSet cds;

            // Timeout of -1 will wait for an item or IsCompleted == true.

            if(commits.TryTake(out cds, -1))
            {
                // Got at least one item, write it.
                db.Write(cds);

                // Continue dequeuing until the queue is empty, where it will
                // timeout instantly and return false, or until we've dequeued
                // 100 items.

                for(int i = 1; i < 100 && commits.TryTake(out cds, 0); ++i)
                {
                    db.Write(cds);
                }

                // Now that we're waiting for more items or have dequeued 100
                // of them, commit.  More can be continue to be added to the
                // queue by other threads while this commit is processing.

                db.Commit();
            }
        }
    }, TaskCreationOptions.LongRunning);

try
{
    // This is the producer.

    Parallel.For(0, 1000000, i =>
        {
            ComplexDataSet data = GenerateData(i);
            commits.Add(data);
        });
}
finally // put in a finally to ensure the task closes down.
{
    commits.CompleteAdding(); // set commits.IsFinished = true.
    consumer.Wait(); // wait for task to finish committing all the items.
}

【讨论】:

    【解决方案2】:

    在您的示例中,您有 10 000 000 个工作包。每一个都需要分发到一个线程。 假设您没有 真正 大量 cpu 内核,这不是最佳选择。您还必须在每个buffer.SaveDataToBuffer 调用上同步您的线程(通过使用锁)。此外,您应该知道变量r 在年表视图中不一定要加一(例如:Thread1 用 1、2、3 和 Thread2 用 4、5、6 执行 r。按时间顺序这将导致以下结果r 的序列传递给 SaveDataToBuffer 1,4,2,5,3,6(大约)。

    我会让工作包更大,然后一次提交每个包。这还有一个好处是您不必经常锁定/同步所有内容。

    这是一个例子:

    int total = 10000000;
    int step = 1000;
    
    Parallel.For(0, total / step, (r, state) =>
    {
        int start = r * start;
        int end = start + step;
    
        ComplexDataSet[] result = new ComplexDataSet[step];
    
        for (int i = start; i < end; i++)
        {
            result[i - start] = GenerateData(i);
        }
    
        Commit(result);
    });
    

    在这个例子中,整个工作被分成 10000 个包(并行执行),每个包生成 1000 个数据项并将它们提交到数据库。

    使用此解决方案,Commit 方法可能会成为瓶颈,如果设计不明智的话。最好是在不使用任何锁的情况下使其线程安全。如果您不在需要同步的线程之间使用公共对象,则可以实现这一点。

    例如对于 sql server 后端,这意味着在每个 Commit() 调用的上下文中创建自己的 sql 连接:

    private void Commit(ComplexDataSet[] data)
    {
        using (var connection = new SqlConnection("connection string..."))
        {
            connection.Open();
    
            // insert your data here...
        }
    }
    

    【讨论】:

    • 是的,我很清楚 1000 多个并行 for 循环可能效率不高 - 我正在尝试自定义分区,看看是否有帮助。然而,这是另一个问题。我遇到的主要问题是如何优化数据库的事务数量。由于性能至关重要,因此我使用了一个名为 MongoDb 的 NoSQL 数据库。为此,我仍然需要一个具有良好提交触发机制的良好线程安全队列。你提到可以在没有锁的情况下完全做到这一点……你是什么意思?愿意举个例子吗?
    • Parallel.For 使用线程池——它为每个项目创建单独的线程。它甚至没有为每个循环添加一个新的池工作项——它把它们分块。不过,手动分块仍然是一个好主意,因为您可以避免在线程之间共享单个 DataBuffer 的开销。
    【解决方案3】:

    与其增加软件的复杂性,不如考虑简化。您可以将代码重构为三个部分:

    1. 排队的工人

      这是 Parallel.For 中的并发 GenerateData,它执行一些繁重的计算并生成 ComplexDataSet

    2. 实际队列

      存储来自 [1] 的结果的并发队列 - 这么多 ComplexDataSet。在这里,我假设 ComplexDataSet 的一个实例实际上并不消耗资源并且相当轻巧。只要队列是并发的,它就会支持并行“插入”和“删除”。

    3. 出队的工人

      从处理队列 [2] 中获取 ComplexDataSet 的一个实例并将其放入并发包(或其他存储)中的代码。一旦袋子有 N 个您阻止的项目,停止出队,将袋子的内容刷新到数据库中并清除它。最后,您解除阻塞并恢复出队。

    这是一些元代码(它仍然可以编译,但需要改进)

    [1]

    // [1] - Class is responsible for generating complex data sets and 
    // adding them to processing queue
    class EnqueueWorker
    {
        //generate data and add to queue
        internal void ParrallelEnqueue(ConcurrentQueue<ComplexDataSet> resultQueue)
        {
            Parallel.For(1, 10000, (i) =>
            {
                ComplexDataSet cds = GenerateData(i);
                resultQueue.Enqueue(cds);
    
            });
        }
    
        //generate data
        ComplexDataSet GenerateData(int i)
        {
            return new ComplexDataSet();
        }
    }
    

    [3]

    //[3] This guy takes sets from the processing queue and flush results when 
    // N items have been generated
    class DequeueWorker
    {
        //buffer that holds processed dequeued data
        private static ConcurrentBag<ComplexDataSet> buffer;
    
        //lock to flush the data to the db once in a while
        private static object syncRoot = new object();
    
        //take item from processing queue and add it to internal buffer storage
        //once buffer is full - flush it to the database
        internal void ParrallelDequeue(ConcurrentQueue<ComplexDataSet> resultQueue)
        {
            buffer = new ConcurrentBag<ComplexDataSet>();
            int N = 100;
    
            Parallel.For(1, 10000, (i) =>
            {
                //try dequeue
                ComplexDataSet cds = null;
    
                var spinWait = new SpinWait();
    
                while (cds == null)
                {
                    resultQueue.TryDequeue(out cds);
                    spinWait.SpinOnce();
                }
    
                //add to buffer
                buffer.Add(cds);
    
                //flush to database if needed
                if (buffer.Count == N)
                {
                    lock (syncRoot)
                    {
                        IEnumerable<ComplexDataSet> data = buffer.ToArray();
    
                        // flush data to database
    
                        buffer = new ConcurrentBag<ComplexDataSet>();
                    }
                }
    
            });
        }        
    }
    

    [2]及用法

    class ComplexDataSet { }
    
    class Program
    {
        //processing queueu - [2]
        private static ConcurrentQueue<ComplexDataSet> processingQueue;
    
        static void Main(string[] args)
        {
            // create new processing queue - single instance for whole app
            processingQueue = new ConcurrentQueue<ComplexDataSet>();
    
            //enqueue worker
            Task enqueueTask = Task.Factory.StartNew(() =>
                {
                    EnqueueWorker enqueueWorker = new EnqueueWorker();
                    enqueueWorker.ParrallelEnqueue(processingQueue);
                });
    
            //dequeue worker
            Task dequeueTask = Task.Factory.StartNew(() =>
            {
                DequeueWorker dequeueWorker = new DequeueWorker();
                dequeueWorker.ParrallelDequeue(processingQueue);
            });            
        }
    }
    

    【讨论】:

    • 嗯,好的,你能用一些代码说明一下吗?我不确定我是否完全跟着你..
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-12-26
    • 1970-01-01
    • 2012-09-04
    • 1970-01-01
    • 2012-04-02
    • 1970-01-01
    • 2022-11-11
    相关资源
    最近更新 更多