提取拉链,解析文件并展平为CSV

时间:2017-02-22 14:05:11

标签: c# performance task-parallel-library tpl-dataflow

我正在尝试最大化以下任务的性能:

  • 枚举zip文件目录
  • 在内存中提取拉链,查找.json个文件(处理嵌套拉链)
  • 解析json个文件
  • json文件中的属性写入聚合的.CSV文件

我想要的TPL布局是:

producer -> parser block -> batch block -> csv writer block

这个想法是单个生产者提取拉链并找到json文件,将文本发送到并行运行的解析器块(多用户)。批处理块分组为200个批处理,写入程序块每次调用将200行转储到CSV文件。

问题:

  • jsonParseBlock 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);
                }
            }
        }
    }
    

UPDATE1

我添加了async,但我不清楚如何等待所有数据流块完成(c#,async和tpl新增功能)。我基本上想说,“继续运行,直到所有队列/块都为空”。我添加了以下“等待”代码,似乎正在运行。

// wait for crawler to finish
crawlerTask.Wait(); 
// wait for the last block
flatWriterBlock.Completion.Wait(); 

2 个答案:

答案 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);
        }
    }
}

你需要一直重新启动异步,但这应该让你开始。