并行 LINQ: AsParallel().forAll() 空掉了一些对象

5

我这里有一个非常奇怪的情况,似乎 forAll() plinq-query 移除了我的一些自定义对象,说实话,我不知道为什么会这样。

var myArticles = data.FilterCustomerArticles([]params]).ToList(); //always returns 201 articles

result.Articles = new List<ArticleMinimal>();

try
{
    myArticles.AsParallel().ForAll(article =>
                    {
                        result.Articles.Add(new ArticleMinimal()
                        {
                            ArticleNumber = article.ArticleNumber,
                            Description = article.Description,
                            IsMaterial = false,
                            Price = article.PortionPrice.HasValue ? article.PortionPrice.Value : decimal.Zero,
                            Quantity = 1,
                            ValidFrom = new DateTime(1900, 1, 1),
                            ValidTo = new DateTime(2222, 1, 1)
                        });
                    });

}
catch (Exception ex)
{
    ...
}

上面的代码每次调用时返回不同数量的结果。它应该返回201个ArticleMinimal对象,但实际上它会返回200、189、19x等。有时会返回201,但没有任何异常或错误信息。它只是返回少于应该返回的对象。

将代码更改为“老派”的foreach循环后,我总是得到了期望的201个对象。

有效代码:

var myArticles = data.FilterCustomerArticles([]params]).ToList(); //always returns 201 articles

result.Articles = new List<ArticleMinimal>();

try
{
    foreach (var article in myArticles) { 
        result.Articles.Add(new ArticleMinimal()
                        {
                            ArticleNumber = article.ArticleNumber,
                            Description = article.Description,
                            IsMaterial = false,
                            Price = article.PortionPrice.HasValue ? article.PortionPrice.Value : decimal.Zero,
                            Quantity = 1,
                            ValidFrom = new DateTime(1900, 1, 1),
                            ValidTo = new DateTime(2222, 1, 1)
                        });
    }

}
catch (Exception ex)
{
    ...
}

此外,在更多的代码行后,我又有了一个像这样的forAll

try
{
    result.Articles.AsParallel().ForAll(article =>
                {
                    if (article.Weight != null){
                        ...
                    }
                });
}
catch (Exception)
{
    ...
}

使用第一个forAll方法会抛出NullReferenceException异常,我认为是因为它期望201个对象,但有些列表条目为空。现在我的实际问题是:为什么第一个forAll方法返回的对象比应该返回的少?我想到的唯一线索就是内联声明new ArticleMinimal(){ ...}); - 但即使这是原因,对我来说也很奇怪。在使用PLINQ的同时是否不可能这样做呢?我只是猜测。希望你能帮助解答。最好的问候,Dom

2
你正在从多个线程操作一个共享对象(result.Articles 集合),如果这个对象不是线程安全的,那么很可能会破坏该对象。 - Lasse V. Karlsen
现在,说完了并留下了我的答案,你真的需要并行处理吗?201篇文章,因此201个简单对象构造根本不需要太多时间,为什么要把事情复杂化呢?就我个人而言,我会使用我的答案中的代码,并删除.AsParallel(),然后保留其余部分,直到你认为这是一个性能问题。也许在ArticleMinimal的构造函数中会发生一些耗时的事情,但也许名称“Minimal”是不正确的? - Lasse V. Karlsen
谢谢你的回答!这201篇文章只是我的开发测试数据。但是关于实际生产系统使用更高数量的问题是很合理的 :) - Dominik
1
好的,这里没有人可以说你是否应该在这里使用并行处理,所以如果你意识到了区别和可能性,那就是唯一重要的事情。 - Lasse V. Karlsen
2个回答

9

如果您在多个线程中操作result.Articles,很可能会破坏内部结构,正如您所观察到的那样。

相反,将并行工作流转换为一个管道,返回创建的对象:

result.Articles.AddRange(myArticles.AsParallel().Select(article =>
    new ArticleMinimal()
    {
        ArticleNumber = article.ArticleNumber,
        Description = article.Description,
        IsMaterial = false,
        Price = article.PortionPrice.HasValue ? article.PortionPrice.Value : decimal.Zero,
        Quantity = 1,
        ValidFrom = new DateTime(1900, 1, 1),
        ValidTo = new DateTime(2222, 1, 1)
    })
);
.Select 这里是在 .AsParallel() 返回的 ParallelQuery 上执行,它将并行地对项目进行处理。
然而,.AddRange 会要求使用 ParallelQuery.GetEnumerator() 来返回所有项目被收集到一个长集合中,以得到您想要的结果。
根本区别在于,.AddRange() 可能不会添加任何内容,直到所有并行任务开始完成,而如果您添加了适当的锁定,您的方法将会在生成项目时向集合中添加项目。然而,除非您想观察项目在生成时流入集合,否则这在您的情况下意义不大。

请看一下我对你问题的第二条评论。 - Lasse V. Karlsen
如果需要的话,也可以看一下@SchlaWiener提供的另一种方法。 - Lasse V. Karlsen

5

List.Add 不是线程安全的。请参考 https://dev59.com/EV_Va4cB1Zd3GeqPPxny#8796528

使用 lock

lock (result.Articles)
{
    result.Articles.Add(...);
}

或者使用线程安全的集合。我会使用一个临时集合,最后使用 result.Articles.AddRange(...)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接