我有多个枚举器枚举平面文件。我最初在并行调用中有每个枚举器,每个Action都添加到BlockingCollection<Entity>
,并且该集合返回一个ConsumingEnumerable();
public interface IFlatFileQuery
{
IEnumerable<Entity> Run();
}
public class FlatFile1 : IFlatFileQuery
{
public IEnumerable<Entity> Run()
{
// loop over a flat file and yield each result
yield return Entity;
}
}
public class Main
{
public IEnumerable<Entity> DoLongTask(ICollection<IFlatFileQuery> _flatFileQueries)
{
// do some other stuff that needs to be returned first:
yield return Entity;
// then enumerate and return the flat file data
foreach (var entity in GetData(_flatFileQueries))
{
yield return entity;
}
}
private IEnumerable<Entity> GetData(_flatFileQueries)
{
var buffer = new BlockingCollection<Entity>(100);
var actions = _flatFileQueries.Select(fundFileQuery => (Action)(() =>
{
foreach (var entity in fundFileQuery.Run())
{
buffer.TryAdd(entity, Timeout.Infinite);
}
})).ToArray();
Task.Factory.StartNew(() =>
{
Parallel.Invoke(actions);
buffer.CompleteAdding();
});
return buffer.GetConsumingEnumerable();
}
}
然而经过一些测试后发现,下面的代码更改速度提高了大约20-25%。
private IEnumerable<Entity> GetData(_flatFileQueries)
{
return _flatFileQueries.AsParallel().SelectMany(ffq => ffq.Run());
}
代码更改的问题在于它等待所有平面文件查询被枚举,然后才返回整个批次,然后可以枚举和生成。
是否有可能以某种方式在上面的代码中产生更快的速度?
我应该补充一点,所有平面文件查询的组合结果最多可能只有1000个实体。
修改: 将其更改为以下内容对运行时间没有影响。 (R#甚至建议回到它的方式)
private IEnumerable<Entity> GetData(_flatFileQueries)
{
foreach (var entity in _flatFileQueries.AsParallel().SelectMany(ffq => ffq.Run()))
{
yield return entity;
}
}
答案 0 :(得分:4)
代码更改的问题在于它等待所有平面文件查询被枚举,然后才返回整个批次,然后可以枚举和生成。
让我们通过一个简单的例子来证明它是错误的。首先,让我们创建一个TestQuery
类,它将在给定时间后产生一个实体。其次,让我们并行执行几个测试查询,并测量产生结果所需的时间。
public class TestQuery : IFlatFileQuery {
private readonly int _sleepTime;
public IEnumerable<Entity> Run() {
Thread.Sleep(_sleepTime);
return new[] { new Entity() };
}
public TestQuery(int sleepTime) {
_sleepTime = sleepTime;
}
}
internal static class Program {
private static void Main() {
Stopwatch stopwatch = Stopwatch.StartNew();
var queries = new IFlatFileQuery[] {
new TestQuery(2000),
new TestQuery(3000),
new TestQuery(1000)
};
foreach (var entity in queries.AsParallel().SelectMany(ffq => ffq.Run()))
Console.WriteLine("Yielded after {0:N0} seconds", stopwatch.Elapsed.TotalSeconds);
Console.ReadKey();
}
}
此代码打印:
1秒后产生
2秒后产生
3秒后产生
你可以看到这个输出AsParallel()
会尽快产生每个结果,所以一切正常。请注意,根据并行度(例如“2s,5s,6s”,并行度为1,可能会得到不同的时序,有效地使整个操作完全不平行)。此输出来自4芯机器。
如果线程之间没有共同的瓶颈(例如共享锁定资源),那么长处理可能会随核心数量而扩展。您可能希望对算法进行概要分析,以查看是否存在可以使用dotTrace等工具进行改进的慢速部分。
答案 1 :(得分:2)
我认为代码中的任何地方都没有红旗。没有令人发指的低效率。我认为这归结为多个较小的差异。
PLINQ非常擅长处理数据流。在内部,它比逐个添加项目到同步列表更有效。我怀疑您对TryAdd
的调用是一个瓶颈,因为每次调用都需要在内部进行至少两次Interlocked
操作。这些可能会给处理器间内存总线带来巨大负担,因为所有线程都会竞争相同的缓存线。
PLINQ因为内部更便宜,它会做一些缓冲。我确定它不会一个一个地输出项目。可能它会对它们进行批量处理,并通过多种方式分摊同步成本。
第二个问题是BlockingCollection
的有限容量。 100不是很多。这可能会导致很多等待。等待是昂贵的,因为它需要调用内核和上下文切换。
答案 2 :(得分:1)
在任何情况下,我都会选择对我有用的替代方案:
这对我有用:
快速而优异的结果:
Task.Factory.StartNew (() =>
{
Parallel.ForEach<string> (TextHelper.ReadLines(FileName), ProcessHelper.DefaultParallelOptions,
(string currentLine) =>
{
// Read line, validate and enqeue to an instance of FileLineData (custom class)
});
}).
ContinueWith
(
ic => isCompleted = true
);
while (!isCompleted || qlines.Count > 0)
{
if (qlines.TryDequeue (out returnLine))
{
yield return returnLine;
}
}
答案 3 :(得分:0)
默认情况下,ParallelQuery
类将结果累积到输出缓冲区中,该缓冲区的大小由系统选择,然后对查询的使用者可用。为了控制此行为,有一种方法WithMergeOptions
接受类型为ParallelMergeOptions
的值。通过ParallelMergeOptions.NotBuffered
,您将在计算结果后立即获得结果。
private IEnumerable<Entity> GetData(_flatFileQueries)
{
return _flatFileQueries
.AsParallel()
.WithMergeOptions(ParallelMergeOptions.NotBuffered)
.SelectMany(ffq => ffq.Run());
}
未缓冲::使用不带输出缓冲区的合并。计算完结果元素后,立即使该元素对查询的使用者可用。
这仅是一个提示,并且在并行化所有查询时,系统可能不会遵守。