如何报告PLINQ查询的进度?

时间:2019-04-09 02:02:16

标签: c# .net linq parallel-processing

我想报告长期运行的PLINQ查询的进度。

我真的找不到任何允许我执行此操作的本机LINQ方法(对于implemented,是cancellation)。

我已阅读this article,其中显示了常规序列化查询的巧妙扩展功能。

我一直在使用以下代码测试行为。

var progress = new BehaviorSubject<int>(0);
DateTime start = DateTime.Now;
progress.Subscribe(x => { Console.WriteLine(x); });
Enumerable.Range(1,1000000)
    //.WithProgressReporting(i => progress.OnNext(i)) //Beginning Progress
    .AsParallel()
    .AsOrdered()
    //.WithProgressReporting(i => progress.OnNext(i)) //Middle Progress reporting
    .Select(v => { Thread.Sleep(1); return v * v; })
    //.WithProgressReporting(i => progress.OnNext(i)) //End Progress Reporting
    .ToList();
Console.WriteLine("Completed in: " + (DateTime.Now - start).TotalSeconds + " seconds");

编辑:
使用扩展名IEnumerable<T>中间报告进度会删除并行性。

结束中的报告不会在计算并行计算时报告任何进度,因此会在最后迅速报告所有进度。我假设这是将并行计算的结果编译为列表的进度。

我最初认为开始的进度报告导致LINQ无法并行运行。对此进行休眠并阅读了Peter Duniho的评论之后,我发现它实际上是并行运行的,但是我收到了太多的进度报告,以至于处理太多的内容导致我的测试/应用程序显着减慢。

是否存在一种并行/线程安全的方式来增量报告PLINQ的进度,从而使用户可以知道正在进行的进度,而不会对方法运行时间产生重大影响?

2 个答案:

答案 0 :(得分:1)

这个答案可能不那么优雅,但是可以完成工作。

使用PLINQ时,有多个线程处理您的集合,因此使用这些线程来报告进度会导致多个(无序)进度报告,例如0%1%5%4 %3%等...

相反,我们可以使用这些多个线程来更新一个存储进度的共享变量。在我的示例中,它是一个局部变量completed。然后,我们使用Task.Run()生成另一个线程,以0.5s的间隔报告该进度变量。

扩展类:

static class Extensions
    public static ParallelQuery<T> WithProgressReporting<T>(this ParallelQuery<T> sequence, Action increment)
    {
        return sequence.Select(x =>
        {
            increment?.Invoke();
            return x;
        });
    }
}

代码:

static void Main(string[] args)
    {
        long completed = 0;
        Task.Run(() =>
        {
            while (completed < 100000)
            {
                Console.WriteLine((completed * 100 / 100000) + "%");
                Thread.Sleep(500);
            }
        });
        DateTime start = DateTime.Now;
        var output = Enumerable.Range(1, 100000)
            .AsParallel()
            .WithProgressReporting(()=>Interlocked.Increment(ref completed))
            .Select(v => { Thread.Sleep(1); return v * v; })
            .ToList();
        Console.WriteLine("Completed in: " + (DateTime.Now - start).TotalSeconds + " seconds");
        Console.ReadKey();
    }

答案 1 :(得分:0)

此扩展名可以位于LINQ查询的开头或结尾。如果位于开始位置,将立即开始报告进度,但在完成作业之前将错误地报告100%。如果位于末尾,则会准确报告查询的结束,但会延迟报告进度,直到源的第一项完成为止。

public static ParallelQuery<TSource> WithProgressReporting<TSource>(this ParallelQuery<TSource> source,
    long itemsCount, IProgress<double> progress)
{
    int countShared = 0;
    return source.Select(item =>
    {
        int countLocal = Interlocked.Increment(ref countShared);
        progress.Report(countLocal / (double)itemsCount);
        return item;
    });
}

用法示例:

var progress = new Progress<double>(); // Progress captures the System.Threading.SynchronizationContext at construction.
progress.ProgressChanged += (object sender, double e) =>
{
    Console.WriteLine($"Progress: {e:0%}");
};
var numbers = Enumerable.Range(1, 10);
var sum = numbers
.AsParallel()
.WithDegreeOfParallelism(3)
.Select(n => { Thread.Sleep(500); return n; }) // Pretend we are doing something heavy
.WithProgressReporting(10, progress) // <--- the extension method
.Sum();
Console.WriteLine($"Sum: {sum}");

输出:

Query output

有些跳跃,因为有时辅助线程相互抢占。

System.Progress<T>类具有很好的功能,可以在捕获的上下文(通常是UI线程)上调用ProgressChanged事件,因此可以安全地更新UI控件。另一方面,在控制台应用程序中,该事件在ThreadPool上调用,并行查询可能会充分利用该事件,因此该事件将以一定的延迟触发(ThreadPool每500毫秒产生一个新线程)。这就是我在示例中将并行度限制为3的原因,以便保留用于报告进度的空闲线程(我有四核计算机)。