具有Where条件的Parallel.ForEach源列表

时间:2016-09-20 13:41:57

标签: c# concurrency parallel-processing race-condition parallel.foreach

我有一个处理StoreProducts的代码块,然后在每个循环的数据库中添加或更新它们。但这很慢。当我转换代码Parallel.ForEach块时,同时添加和更新相同的产品。我无法弄清楚如何安全地使用以下功能,任何帮助将不胜感激。

var validProducts = storeProducts.Where(p => p.Price2 > 0
                                                     && !string.IsNullOrEmpty(p.ProductAtt08Desc.Trim())
                                                     && !string.IsNullOrEmpty(p.Barcode.Trim()) 
            ).ToList();

var processedProductCodes = new List<string>();

var po = new ParallelOptions()
        {
            MaxDegreeOfParallelism = 4
        };

Parallel.ForEach(validProducts.Where(p => !processedProductCodes.Contains(p.ProductCode)), po,
            (product) =>
{
            lock (_lockThis)
            {
                processedProductCodes.Add(product.ProductCode);
            }

    // Check if Product Exists in Db

    // if product is not in Db Add to Db

    // if product is in Db Update product in Db

}

这里的内容是,列表validProducts可能有多个相同的ProductCode,因此它们是变体,我必须管理即使其中一个正在处理它也不应该再次处理。

因此,在并行foreach'validProducts.Where(p =&gt;!processedProductCodes.Contains(p.ProductCode)'中找到的条件不能像预期的那样正常工作。

2 个答案:

答案 0 :(得分:1)

我的答案大部分都不那么回答您的问题和更多的指导 - 如果您要提供更多技术细节,我可以更准确地提供帮助。

此处Parallel.ForEach可能不是最佳解决方案 - 尤其是当您有共享列表或繁忙服务器时。

您正在锁定写入但不能从该共享列表中读取。所以我很惊讶它不会在Where中投掷。将List<string>转换为ConcurrentDictionary<string, bool>(只是为了创建一个简单的并发哈希表),然后您将获得更好的写入吞吐量,并且在读取期间不会抛出。

但是您将会遇到数据库争用问题(如果使用多个连接),因为您的插入可能仍需要锁定。即使你只是分割工作量,你也会遇到这种情况。此DB锁定可能会导致阻塞/死锁,因此它可能会比原始锁定速度慢。如果使用一个连接,通常无法并行化命令。

我会尝试将大多数插入包装在事务中,包含1000个插入的批次,或者将整个工作负载放入一个批量插入中。然后数据库将数据保存在内存中并在完成时将整个内容提交到磁盘(而不是一次一个记录)。

根据您的典型工作负载,您可能需要尝试不同的存储解决方案。数据库通常不适合插入大量记录...您可能会看到使用替代解决方案(例如键值存储)获得更好的性能。或者将数据放入类似Redis的内容中,然后在后台慢慢地保存到数据库中。

答案 1 :(得分:0)

Parallel.ForEach在内部为每个线程缓冲项目,您可以做的一个选项是切换到不使用缓冲的分区器

var pat = Partitioner.Create(validProducts.Where(p => !processedProductCodes.Contains(p.ProductCode))
                            ,EnumerablePartitionerOptions.NoBuffering);

Parallel.ForEach(pat, po, (product) => ...

这会让你更接近,但你仍然会有一个竞争条件,可以处理两个相同的对象,因为如果你发现重复,你不会突破循环。

更好的选择是将processedProductCodes切换为HashSet<string>并将代码更改为

var processedProductCodes = new HashSet<string>();

var po = new ParallelOptions()
        {
            MaxDegreeOfParallelism = 4
        };

Parallel.ForEach(validProducts, po,
            (product) =>
{
            //You can safely lock on processedProductCodes
            lock (processedProductCodes)
            {
                if(!processedProductCodes.Add(product.ProductCode))
                {
                    //Add returns false if the code is already in the collection.
                    return;
                }
            }

    // Check if Product Exists in Db

    // if product is not in Db Add to Db

    // if product is in Db Update product in Db

}

HashSet具有更快的查找速度,内置于Add函数。