我正在尝试最大化以下任务的性能:
.json
个文件(处理嵌套拉链)json
个文件json
文件中的属性写入聚合的.CSV
文件我想要的TPL布局是:
producer -> parser block -> batch block -> csv writer block
这个想法是单个生产者提取拉链并找到json文件,将文本发送到并行运行的解析器块(多用户)。批处理块分组为200个批处理,写入程序块每次调用将200行转储到CSV文件。
问题:
TransformBlock
占用的时间越长,丢弃的消息就越多。我该如何防止这种情况?我怎样才能更好地利用TPL
来最大限度地提升效果?
class Item
{
public string ID { get; set; }
public string Name { get; set; }
}
class Demo
{
const string OUT_FILE = @"c:\temp\tplflat.csv";
const string DATA_DIR = @"c:\temp\tpldata";
static ExecutionDataflowBlockOptions parseOpts = new ExecutionDataflowBlockOptions() { SingleProducerConstrained=true, MaxDegreeOfParallelism = 8, BoundedCapacity = 100 };
static ExecutionDataflowBlockOptions writeOpts = new ExecutionDataflowBlockOptions() { BoundedCapacity = 100 };
public static void Run()
{
Console.WriteLine($"{Environment.ProcessorCount} processors available");
_InitTest(); // reset csv file, generate test data if needed
// start TPL stuff
var sw = Stopwatch.StartNew();
// transformer
var jsonParseBlock = new TransformBlock<string, Item>(rawstr =>
{
var item = Newtonsoft.Json.JsonConvert.DeserializeObject<Item>(rawstr);
System.Threading.Thread.Sleep(15); // the more sleep here, the more messages lost
return item;
}, parseOpts);
// batch block
var jsonBatchBlock = new BatchBlock<Item>(200);
// writer block
var flatWriterBlock = new ActionBlock<Item[]>(items =>
{
//Console.WriteLine($"writing {items.Length} to csv");
StringBuilder sb = new StringBuilder();
foreach (var item in items)
{
sb.AppendLine($"{item.ID},{item.Name}");
}
File.AppendAllText(OUT_FILE, sb.ToString());
});
jsonParseBlock.LinkTo(jsonBatchBlock, new DataflowLinkOptions { PropagateCompletion = true });
jsonBatchBlock.LinkTo(flatWriterBlock, new DataflowLinkOptions { PropagateCompletion = true });
// start doing the work
var crawlerTask = GetJsons(DATA_DIR, jsonParseBlock);
crawlerTask.Wait();
flatWriterBlock.Completion.Wait();
Console.WriteLine($"ALERT: tplflat.csv row count should match the test data");
Console.WriteLine($"Completed in {sw.ElapsedMilliseconds / 1000.0} secs");
}
static async Task GetJsons(string filepath, ITargetBlock<string> queue)
{
int count = 1;
foreach (var zip in Directory.EnumerateFiles(filepath, "*.zip"))
{
Console.WriteLine($"working on zip #{count++}");
var zipStream = new FileStream(zip, FileMode.Open);
await ExtractJsonsInMemory(zip, zipStream, queue);
}
queue.Complete();
}
static async Task ExtractJsonsInMemory(string filename, Stream stream, ITargetBlock<string> queue)
{
ZipArchive archive = new ZipArchive(stream);
foreach (ZipArchiveEntry entry in archive.Entries)
{
if (entry.Name.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
{
using (TextReader reader = new StreamReader(entry.Open(), Encoding.UTF8))
{
var jsonText = reader.ReadToEnd();
await queue.SendAsync(jsonText);
}
}
else if (entry.Name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
{
await ExtractJsonsInMemory(entry.FullName, entry.Open(), queue);
}
}
}
}
我添加了async
,但我不清楚如何等待所有数据流块完成(c#,async和tpl新增功能)。我基本上想说,“继续运行,直到所有队列/块都为空”。我添加了以下“等待”代码,似乎正在运行。
// wait for crawler to finish
crawlerTask.Wait();
// wait for the last block
flatWriterBlock.Completion.Wait();
答案 0 :(得分:1)
从MSDN,关于DataflowBlock.Post<TInput>
方法:
返回值
输入:System.Boolean
true 如果目标块接受了该项目;否则, false 。
所以,这里的问题是你在没有检查的情况下发送你的消息,管道是否可以接受另一个消息。发生这种情况的原因在于您对块的选择:
new ExecutionDataflowBlockOptions() { BoundedCapacity = 100 }
和这一行:
// this line isn't waiting for long operations and simply drops the message as it can't be accepted by the target block
queue.Post(jsonText);
这里你说处理应该推迟,直到输入队列长度等于100
。在这种情况下,他的Introduction to Dataflow系列中的MSDN或@StephenCleary建议使用简单的解决方案:
但是,可以通过限制缓冲区大小来限制块;在这种情况下,您可以使用
SendAsync
(异步)等待空间可用,然后将数据放入块的输入缓冲区。
因此,正如@JSteward已经建议的那样,您可以在工作者之间引入无限缓冲区以避免消息丢失,这是通常的做法,因为检查Post
方法的结果可能会阻止生产者线程很长一段时间。
关于性能的问题的第二部分是使用面向async
的解决方案(完全符合SendAsync
方法用法),因为您使用所有的I / O操作时间。异步操作基本上是一种表示程序“开始执行此操作并在完成时通知我”的方式。而且,对于此类操作的there is no thread,您可以通过释放线程池来获得管道中的其他操作。
PS:@JSteward为这种方法提供了一个很好的示例代码。
答案 1 :(得分:0)
简而言之,您的发布并忽略了返回值。您有两种选择:添加未绑定的BufferBlock
以保留所有传入数据,或添加await
SendAsync
,以防止丢弃任何邮件。
static async Task ExtractJsonsInMemory(string filename, Stream stream, ITargetBlock<string> queue)
{
var archive = new ZipArchive(stream);
foreach (ZipArchiveEntry entry in archive.Entries)
{
if (entry.Name.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
{
using (var reader = new StreamReader(entry.Open(), Encoding.UTF8))
{
var jsonText = reader.ReadToEnd();
await queue.SendAsync(jsonText);
}
}
else if (entry.Name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
{
await ExtractJsonsInMemory(entry.FullName, entry.Open(), queue);
}
}
}
你需要一直重新启动异步,但这应该让你开始。