使用Rx和Await来逐行完成读取文件的异常

时间:2014-04-14 15:58:50

标签: c# .net async-await system.reactive

我正在学习使用RX并试用这个样本。但是无法修复突出显示的while语句中发生的异常 - while(!f.EndofStream)

我想逐行读取一个巨大的文件 - 对于每一行数据 - 我想在不同的线程中进行一些处理(所以我使用了ObserverOn) 我希望整个事情都是异步的。我想使用ReadLineAsync,因为它返回TASK,所以我可以将它转换为Observables并订阅它。

我想我首先创建的任务线程介于Rx线程之间。但即使我使用currentThread使用Observe和Subscribe,我仍然无法阻止异常。不知道我是如何用Rx完成这个整齐的Aysnc。

想知道整件事情是否可以做得更简单?

    static void Main(string[] args)
    {
        RxWrapper.ReadFileWithRxAsync();
        Console.WriteLine("this should be called even before the file read begins");
        Console.ReadLine();
    }

    public static async Task ReadFileWithRxAsync()
    {
        Task t = Task.Run(() => ReadFileWithRx());
        await t;
    }


    public static void ReadFileWithRx()
    {
        string file = @"C:\FileWithLongListOfNames.txt";
        using (StreamReader f = File.OpenText(file))
        {
            string line = string.Empty;
            bool continueRead = true;

            ***while (!f.EndOfStream)***
            {
                f.ReadLineAsync()
                       .ToObservable()
                       .ObserveOn(Scheduler.Default)
                       .Subscribe(t =>
                           {
                               Console.WriteLine("custom code to manipulate every line data");
                           });
            }

        }
    }

2 个答案:

答案 0 :(得分:10)

异常是InvalidOperationException - 我并不熟悉FileStream的内部,但根据异常消息,这是因为在流上有一个正在进行的异步操作。这意味着在检查ReadLineAsync()之前,您必须等待任何EndOfStream次调用完成。

Matthew Finlay为您的代码提供了一个简洁的重新处理,以解决这个直接问题。但是,我认为它有自己的问题 - 而且还有一个更大的问题需要加以研究。让我们看一下问题的基本要素:

  • 你的文件非常大。
  • 您想要异步处理它。

这表示您不希望整个文件存储在内存中,您希望在处理完成时得到通知,并且您可能希望尽快处理该文件。

两个解决方案都使用线程来处理每一行(ObserveOn将每一行传递给线程池中的一个线程)。这实际上不是一种有效的方法。

看看这两种解决方案,有两种可能性:

  • 甲。读取文件行平均需要更多时间,而不是处理它。
  • B中。读取文件行平均需要时间,而不是处理文件行。

一种。文件读取的行比处理行

在A的情况下,系统在等待文件IO完成时基本上会花费大部分时间空闲。在这种情况下,Matthew的解决方案不会导致内存填满 - 但是如果在紧密循环中直接使用ReadLines会因较少的线程争用而产生更好的结果,那么值得一看。 (ObserveOn将线路推送到另一个线程只会在ReadLines没有在调用MoveNext之前获取线路时才会给你买东西 - 我怀疑它是这样 - 但是测试看看!)

B中。文件读取的行比处理行

更快

在B的情况下(我假设你更有可能得到你所尝试的),所有这些行将开始在内存中排队,对于一个足够大的文件,你最终将大部分内存放在内存中

你应该注意,除非你的处理程序触发异步代码来处理一行,否则所有行都将被串行处理,因为Rx保证OnNext()处理程序调用不会重叠。

ReadLines()方法非常棒,因为它会返回IEnumerable<string>,而您的枚举会驱动读取文件。但是,当您在此上调用ToObservable()时,它将尽可能快地枚举以生成可观察事件 - 在Rx中没有反馈(称为&#34;背压&#34;在被动程序中)以减速这个流程。

问题不在于ToObservable本身 - 它是ObserveOnObserveOn不会阻止在等待它的订阅者完成事件之前调用它的OnNext()处理程序 - 它会尽可能快地将事件排队到目标调度程序。< / p>

如果您删除了ObserveOn,那么 - 只要您的OnNext处理程序是同步的 - 您就会看到每一行都被读取并逐个处理,因为{{1} }正在处理与处理程序相同的线程上的枚举。

如果这不是您想要的,并且您试图通过在订户中触发异步作业来追求并行处理,从而减轻这种影响 - 例如ToObservable()或类似的东西 - 那么事情就不会像你希望的那样顺利。

因为处理线路比读取线路需要更长的时间,所以您将创建越来越多的与传入线路保持同步的任务。线程数将逐渐增加,您将使线程池匮乏。

在这种情况下,Rx并不是非常适合。

您可能需要的是少量工作线程(每个处理器核心可能有1个),它们一次获取一行代码,并限制内存中文件的行数。

这可能是一种简单的方法,它将内存中的行数限制为固定数量的工作者。它是一个基于拉的解决方案,在这种情况下这是一个更好的设计:

Task.Run(() => /* process line */

并像这样使用它:

private Task ProcessFile(string filePath, int numberOfWorkers)
{
    var lines = File.ReadLines(filePath);       

    var parallelOptions = new ParallelOptions {
        MaxDegreeOfParallelism = numberOfWorkers
    };  

    return Task.Run(() => 
        Parallel.ForEach(lines, parallelOptions, ProcessFileLine));
}

private void ProcessFileLine(string line)
{
    /* Your processing logic here */
    Console.WriteLine(line);
}

最终笔记

在Rx中有一些处理背压的方法(搜索SO以进行一些讨论) - 但它并不是Rx处理得好的东西,我认为最终解决方案的可读性低于上面的替代方案。您还可以查看许多其他方法(基于actor的方法,如TPL数据流,或用于高性能无锁方法的LMAX Disruptor样式环缓冲区),但 pull 的核心思想是队列将很普遍。

即使在这个分析中,我也很方便地了解你正在做什么来处理文件,并且默认假设每行的处理是计算绑定的并且是真正独立的。如果有工作要合并结果和/或IO活动来存储输出,那么所有的赌注都会关闭 - 你需要仔细检查这方面的效率。

在大多数情况下,正在考虑并行执行工作,通常会有很多变量,最好衡量每种方法的结果,以确定最佳方法。测量是一门艺术 - 确保测量真实场景,对每次测试的多次运行取平均值,并在运行之间正确地重置环境(例如消除缓存效应),以减少测量误差。

答案 1 :(得分:2)

我没有查看导致您的异常的原因,但我认为最好的方法是:

File.ReadLines(file)
  .ToObservable()
  .ObserveOn(Scheduler.Default)
  .Subscribe(Console.Writeline);

注意:ReadLines与ReadAllLines的不同之处在于,它会在不读取整个文件的情况下开始产生,这就是您想要的行为。