公平的警告:这是一个关于方法的问题,至少是良好实践的问题。这里的问题不是语法,而是方法。
我必须非常快速地处理大量记录,并向用户提供一组转换后的记录。我想知道是否有人对最有效的方法有实用的建议。
这是场景:
我需要执行一组相对简单的逻辑: 连接到数据库->读取记录->转换每个记录->将输出记录提供给使用者
此逻辑需要从库中获得-内部逻辑对使用者完全隐藏。 (消费者不知道会发生某种转换-他认为他只是在遍历一堆对象。)
通常,我将使用如下方法创建IEnumerable类:
public class TransformingReader<T> where T:class,new()
{
...
...
...
public IEnumerator<T> GetEnumerator()
{
var items = _connection<dynamic>.GetData();
foreach (var item in items)
{
T transformed = _complexTask.Transform(item);
yield return transformed;
}
}
}
(这里使用动态类只是为了说明)
使用上面的类,消费者:
foreach(var item in new TransformingReader<TransactionAnalysis>())
{
...
DoStuff(item);
...
}
事实:
我每天要处理数百万条记录-因此处理量是一个大问题。
用户的DoStuff()函数将需要一些时间才能完成。我没有真正的方法来预测他们的工作将有多复杂,但是与我的工作相比,它必然需要更多的IO投入。
我在一个相对受限的环境中工作-因此没有足够的可用内存,并且其他应用程序位于同一台计算机上。因此,我需要表现得负责任。 (我没有在爷爷的笔记本电脑上运行-但我仍然需要编写不贪婪的明智代码)
想法:
我想尝试并行化Transform()函数,以便可以利用DoStuff()忙于转换下一条记录的时间。这样,希望我总是(经常?)在用户要求下一个记录时准备好新记录。
我希望在消费者方面保持简单的foreach语法。消费者无需知道我正在幕后努力。
任何有关如何解决此类问题的想法都将不胜感激。具体来说,也许有一种我不知道的模式可以帮助解决这个问题?
答案 0 :(得分:1)
这听起来像Produce-consumer problem。
一种解决方案是创建一个用于检索和转换数据的线程,即生产者线程。然后在其他线程(可能是主线程)中,运行消费者,即用户DoStuff(item)
。将有一个队列(很可能是concurrent queue)将用于在线程之间进行通信。
从用户的角度来看,您仍然可以提供数据作为枚举器,该数据将从队列中读取,在队列为空时阻塞,并在读取某个表示输入结束的预定值时终止(有时称为 poison药)。
内存占用量由队列的大小决定,因此您可以根据需要进行调整。
此模式使您可以扩大生产者和消费者的数量,因此您可以同时Transform()
同时DoStuff()
同时使用多个项目MERGE INTO destination dst
USING source src
ON ( dst.some_columns = src.some_columns )
WHEN MATCHED THEN
UPDATE SET columns_to_update = dst.columns_to_update_from
WHEN NOT MATCHED THEN
INSERT ( some_columns, columns_to_update )
VALUES ( dst.some_columns, dst.columns_to_update_from )
。
根据您的描述,可能可以使用一个Parallel LINQ语句来解决您的问题(幕后使用上述解决方案的变体)。
答案 1 :(得分:1)
是的,它是生产者-消费者模式。
请参见Pipelines如何实现它。
var records = new BlockingCollection<SomeRecord>();
var outputs = new BlockingCollection<SomeResult>();
var readRecords = Task.Run(async () =>
{
using (var conn = new SqlConnection("..."))
{
conn.Open();
using (var cmd = conn.CreateCommand())
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
var record = new SomeRecord { Prop = reader.GetValue(0) };
records.Add(record);
}
}
}
});
var transformRecords = Task.Run(() =>
{
foreach (var record in records.GetConsumingEnumerable())
{
// transform record
outputs.Add(new SomeResult());
}
});
var consumeResults = Task.Run(() =>
{
foreach (var result in outputs.GetConsumingEnumerable())
{
// ...
}
});
Task.WaitAll(readRecords, transformRecords, consumeResults);
如有必要,可以轻松增加流水线级数,增加新任务。
转换很容易并行化:
records.GetConsumingEnumerable()
.AsParallel()
.AsOrdered() // if you want to keep order
如果其中一项任务比其他任务快得多并且阻塞了内存,则可以限制其收集的容量:
var records = new BlockingCollection<SomeRecord>(boundedCapacity: 50);