CsvHelper - 异步读取流

时间:2016-05-06 02:14:58

标签: c# asynchronous stream async-await csvhelper

我有一个服务,它接受包含需要批量插入数据库的CSV数据的输入流,并且我的应用程序尽可能使用async / await。

过程是:使用CsvHelper的CsvParser解析流,将每一行添加到DataTable,使用SqlBulkCopy将DataTable复制到数据库。

数据可以是任何大小,所以我想避免一次将整个内容读入内存 - 显然我最终会在DataTable中拥有所有数据,因此在内存中基本上会有2个副本。

我想尽可能异步地完成所有这些,但CsvHelper没有任何异步方法,所以我想出了以下解决方法:

using (var inputStreamReader = new StreamReader(inputStream))
{
    while (!inputStreamReader.EndOfStream)
    {
        // Read line from the input stream
        string line = await inputStreamReader.ReadLineAsync();

        using (var memoryStream = new MemoryStream())
        using (var streamWriter = new StreamWriter(memoryStream))
        using (var memoryStreamReader = new StreamReader(memoryStream))
        using (var csvParser = new CsvParser(memoryStreamReader))
        {
            await streamWriter.WriteLineAsync(line);
            await streamWriter.FlushAsync();

            memoryStream.Position = 0;

            // Loop through all the rows (should only be one as we only read a single line...)
            while (true)
            {
                var row = csvParser.Read();

                // No more rows to process
                if (row == null)
                {
                    break;
                }

                // Add row to DataTable
            }
        }
    }
}

这个解决方案有什么问题吗?它甚至是必要的吗?我已经看到CsvHelper开发人员没有特别添加异步功能(https://github.com/JoshClose/CsvHelper/issues/202),但我并没有真正遵循不这样做的原因。

编辑:我刚刚意识到这个解决方案不适用于列包含换行的实例:(猜猜我只需要将整个输入流复制到MemoryStream或其他东西

EDIT2:更多信息。

这是一个库中的异步方法,我试图一直向下执行async。它可能会被MVC控制器使用(如果我只是想从UI线程中卸载它,我只需要Task.Run()它)。大多数情况下,该方法将等待外部源,如数据库/ DFS,我希望线程可以被释放。

CsvParser.Read()将阻止即使阻塞正在读取流(例如,如果我试图读取的数据驻留在世界另一端的服务器上),而如果CsvHelper要实现一个使用TextReader.ReadAsync()的异步方法,然后我不会被阻止等待我的数据从迪拜到达。据我所知,我不是要求同步方法的异步包装器。

2 个答案:

答案 0 :(得分:2)

Eric lippert解释了使用a metaphor of cooking a meal in a restaurant的async-await的用处。根据他的解释,如果你的线程没有别的办法,那么异步做某事是没有用的。

另外,请注意,当您的线程正在执行某些操作时,它无法执行其他操作。只有你的线程在等待它可以做其他事情。在您的过程中等待的一件事是读取文件。当线程正在读取文件时,它必须等待几次才能读取文件部分。在阅读过程中,它可以执行其他操作,例如解析读取的CSV数据并将解析的数据发送到目的地。

解析数据不是您的线程必须等待其他一些进程完成的过程,就像读取文件或将数据发送到数据库时必须要做的那样。这就是解析过程没有异步版本的原因。正常的async-await无助于保持线程繁忙,因为在解析过程中没有什么可以等待的,所以在解析过程中你的线程没有时间做其他事情。

你当然可以使用Task.Run(()=> ParseReadData(...))将解析过程转换为等待任务,并等待此任务完成,但是在Eric Lippert的餐厅这个比喻中当你坐在柜台后面什么都不做的时候,他会解冻一个厨师去做这个工作。

但是,如果您的线程有一些有意义的事情要做,在解析读取的CSV数据时,比如响应用户输入,那么在单独的任务中启动解析可能会很有用。

如果你的完整阅读 - 解析 - 更新数据库进程不需要与用户交互,但你需要你的线程在执行这个过程时可以自由地做其他事情,那么考虑将整个过程放在一个单独的任务中,并且在没有等待的情况下开始任务。在这种情况下,您只使用您的接口线程来启动其他任务,并且您的接口线程可以自由地执行其他操作。与您的流程总时间相比,开始这项新任务的成本相对较低。

再一次:如果您的线程没有其他任何事情可做,请让此线程进行处理,不要启动其他任务来执行此操作。

答案 1 :(得分:1)

这是一篇关于在同步方法上公开异步包装器的好文章,以及为什么CsvHelper没有这样做。 http://blogs.msdn.com/b/pfxteam/archive/2012/03/24/10287244.aspx

如果您不想阻止UI线程,请在后台线程上运行处理。

CsvHelper拉入数据缓冲区。缓冲区的大小是您可以根据需要更改的设置。如果您的服务器位于世界的另一端,它会缓冲一些数据,然后读取它。很可能,在使用缓冲区之前,它会进行多次读取。

CsvHelper也会产生记录,所以如果你实际上没有记录,那么就不会读取任何内容。如果只读取几行,则只读取该文件的大部分(实际上是缓冲区大小)。

如果你担心记忆,可以选择几个简单的选项。

  1. 缓冲数据。您可以一次批量复制100或1000行,而不是整个文件。继续这样做,直到文件完成。
  2. 使用FileStream。如果由于某种原因需要一次读取整个文件,请改用FileStream并将整个文件写入光盘。它会慢一点,但你不会使用一堆内存。