我想报告长期运行的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的进度,从而使用户可以知道正在进行的进度,而不会对方法运行时间产生重大影响?
答案 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}");
输出:
有些跳跃,因为有时辅助线程相互抢占。
System.Progress<T>
类具有很好的功能,可以在捕获的上下文(通常是UI线程)上调用ProgressChanged
事件,因此可以安全地更新UI控件。另一方面,在控制台应用程序中,该事件在ThreadPool上调用,并行查询可能会充分利用该事件,因此该事件将以一定的延迟触发(ThreadPool每500毫秒产生一个新线程)。这就是我在示例中将并行度限制为3的原因,以便保留用于报告进度的空闲线程(我有四核计算机)。