我有一个服务,它接受包含需要批量插入数据库的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()的异步方法,然后我不会被阻止等待我的数据从迪拜到达。据我所知,我不是要求同步方法的异步包装器。
答案 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也会产生记录,所以如果你实际上没有记录,那么就不会读取任何内容。如果只读取几行,则只读取该文件的大部分(实际上是缓冲区大小)。
如果你担心记忆,可以选择几个简单的选项。