已排序的 PLINQ ForAll

12

MSDN文档关于PLINQ中保留顺序的说明中,对ForAll()给出了以下内容:

  • 当源序列有序时的结果:在并行执行时是不确定的
  • 当源序列无序时的结果:在并行执行时是不确定的

这是否意味着ForAll方法的有序执行从来没有被保证过?

我之前没有使用过PLINQ,但下面的Code Review question似乎是适合它的。在我的答案底部我写道:

Events.AsParallel().AsOrdered().ForAll( eventItem =>
{
    ...
} );    

阅读文档后,我认为AsOrdered()不会改变任何内容?我还怀疑之前的查询无法替换需要保持顺序的简单for循环?可能会发生对StringBuilder的并行调用,导致输出错误?

2
如果代码是顺序执行的,那么并行LINQ有什么用处呢? - CodesInChaos
6个回答

18

通常情况下,顺序保留仅适用于结果——即输入可以以任何顺序进行处理,但以原始顺序返回。

ForAll 不返回任何东西,据我所知它实际上没有任何效果。

要使排序适用于处理,唯一的方法是在处理第1项之前完成第0项,在处理第2项之前完成第1项等,此时您将没有并行性。


好的,我的怀疑是正确的。谢谢解释。在给定的例子中,我相信并行计算仍然应该是一个选项,当所有字符串可以单独解析,然后按顺序合并。什么样的构造最适合这种情况? - Steven Jeuris
2
Steven: 你可以使用PLINQ只进行解析,让它按顺序给出结果,并使用普通的foreach循环来执行实际的追加操作。 - Jon Skeet

8
正如其他人正确回答的那样,ForAll方法不能保证以任何特定顺序执行可枚举元素的操作,并会默默忽略AsOrdered()方法调用。
为了让读者更好地执行一个操作,以一种尽可能接近原始顺序的方式(在并行处理上下文中合理的情况下),以下扩展方法可能会有所帮助。
public static void ForAllInApproximateOrder<TSource>(this ParallelQuery<TSource> source, Action<TSource> action) {

    Partitioner.Create( source )
               .AsParallel()
               .AsOrdered()
               .ForAll( e => action( e ) );

}

这可以按如下方式使用:
orderedElements.AsParallel()
               .ForAllInApproximateOrder( e => DoSomething( e ) );

需要注意的是,上述扩展方法使用的是 PLINQ 的 ForAll 而不是 Parallel.ForEach,因此继承了 PLINQ 内部使用的线程模型(这与 Parallel.ForEach 使用的模型不同 -- 依我的经验,默认情况下要温和一些)。以下是使用 Parallel.ForEach 的类似扩展方法。

public static void ForEachInApproximateOrder<TSource>(this ParallelQuery<TSource> source, Action<TSource> action) {

    source = Partitioner.Create( source )
                        .AsParallel()
                        .AsOrdered();

    Parallel.ForEach( source , e => action( e ) );

}

这可以按照以下方式使用:
orderedElements.AsParallel()
               .ForEachInApproximateOrder( e => DoSomething( e ) );

当使用以上任一扩展方法时,没有必要将AsOrdered()链接到您的查询中,因为它会在内部自动调用。

我发现这些方法对于处理具有粗粒度意义的元素非常有用。例如,按照最旧的记录开始处理并向最新的记录进行处理可能是有用的。在许多情况下,并不需要记录的确切顺序——只要较旧的记录通常在较新的记录之前处理即可。同样,可以处理具有低/中/高优先级级别的记录,以便在大多数情况下先处理高优先级记录,而边缘情况稍微滞后。


Mr. Fantastic,使用显式的Partitioner.Create()是否必要,还是只需像Parallel.ForEach(source.AsParallel().AsOrdered(), e => ...)一样全部内联?谢谢! - crokusek

6
AsOrdered()不会改变任何内容 - 如果您想在并行查询的结果上强制执行顺序,则可以简单地使用foreach()ForAll()用于利用并行性,这意味着一次对集合中多个项执行副作用。实际上,排序仅适用于查询的结果(结果集合中的项目顺序),但这与ForAll()无关,因为ForAll()根本不影响顺序。
在PLINQ中,目标是最大化性能,同时保持正确性。查询应该尽可能快地运行,但仍然产生正确的结果。在某些情况下,正确性需要保留源序列的顺序。
请注意,ForAll()不会转换集合(即不会投影到新集合),它纯粹用于在PLINQ查询的结果上执行副作用。

4
这是否意味着 ForAll 方法的有序执行从未得到保证?
是的,顺序不能保证。
并行化意味着将工作分配给不同的线程,它们的独立输出稍后会合并。
如果需要对输出进行排序,则不要使用 PLinq,或者添加一些后续步骤来恢复排序。
此外,如果您从 plinq 执行中访问像 StringBuilder 这样的对象,请确保这些对象是线程安全的,并且还要注意这种线程安全性可能实际上会使 plinq 比非并行 linq 更慢。

2
现在作为扩展方法:
它将在多个核心上进行处理,然后对结果进行排序,因此有排序的开销。这里有一个关于简单循环和并行计算基准测试的答案
 public static IEnumerable<T1> OrderedParallel<T, T1>(this IEnumerable<T> list, Func<T, T1> action)
    {
        var unorderedResult = new ConcurrentBag<(long, T1)>();
        Parallel.ForEach(list, (o, state, i) =>
        {
            unorderedResult.Add((i, action.Invoke(o)));
        });
        var ordered = unorderedResult.OrderBy(o => o.Item1);
        return ordered.Select(o => o.Item2);
    }

使用如下:
var result = Events.OrderedParallel(eventItem => ...);

希望这可以为您节省一些时间。

-1

ForAll 在多个线程中并行运行操作。在任何给定时刻,多个操作将同时运行,在这种情况下,“顺序”的概念不适用。要按顺序运行操作,必须按顺序运行它们,而最简单的方法是在单个线程中运行它们。这可以通过在标准的foreach循环中枚举查询结果来实现:

var query = Events.AsParallel().AsOrdered();
foreach (var eventItem in query)
{
    // do something with the eventItem
}

如果您更喜欢流畅的ForAll语法,您可以在项目中添加一个静态类,并使用下面的ForEach扩展方法:

public static void ForEach<TSource>(this IEnumerable<TSource> source,
    Action<TSource> action)
{
    foreach (TSource item in source)
    {
        action(item);
    }
}

然后像这样使用:

Events.AsParallel().AsOrdered().ForEach(eventItem =>
{
    // do something with the eventItem
});

需要注意的是,在给定的示例中,使用Parallel LINQ是多余的。查询Events.AsParallel().AsOrdered()对源可枚举对象不执行任何转换,因此没有实际的计算发生。您可以删除.AsParallel().AsOrdered()部分并获得相同的结果。

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