我正在学习使用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");
});
}
}
}
答案 0 :(得分:10)
异常是InvalidOperationException
- 我并不熟悉FileStream的内部,但根据异常消息,这是因为在流上有一个正在进行的异步操作。这意味着在检查ReadLineAsync()
之前,您必须等待任何EndOfStream
次调用完成。
Matthew Finlay为您的代码提供了一个简洁的重新处理,以解决这个直接问题。但是,我认为它有自己的问题 - 而且还有一个更大的问题需要加以研究。让我们看一下问题的基本要素:
这表示您不希望整个文件存储在内存中,您希望在处理完成时得到通知,并且您可能希望尽快处理该文件。
两个解决方案都使用线程来处理每一行(ObserveOn
将每一行传递给线程池中的一个线程)。这实际上不是一种有效的方法。
看看这两种解决方案,有两种可能性:
在A的情况下,系统在等待文件IO完成时基本上会花费大部分时间空闲。在这种情况下,Matthew的解决方案不会导致内存填满 - 但是如果在紧密循环中直接使用ReadLines
会因较少的线程争用而产生更好的结果,那么值得一看。 (ObserveOn
将线路推送到另一个线程只会在ReadLines
没有在调用MoveNext
之前获取线路时才会给你买东西 - 我怀疑它是这样 - 但是测试看看!)
在B的情况下(我假设你更有可能得到你所尝试的),所有这些行将开始在内存中排队,对于一个足够大的文件,你最终将大部分内存放在内存中
你应该注意,除非你的处理程序触发异步代码来处理一行,否则所有行都将被串行处理,因为Rx保证OnNext()
处理程序调用不会重叠。
ReadLines()
方法非常棒,因为它会返回IEnumerable<string>
,而您的枚举会驱动读取文件。但是,当您在此上调用ToObservable()
时,它将尽可能快地枚举以生成可观察事件 - 在Rx中没有反馈(称为&#34;背压&#34;在被动程序中)以减速这个流程。
问题不在于ToObservable
本身 - 它是ObserveOn
。 ObserveOn
不会阻止在等待它的订阅者完成事件之前调用它的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的不同之处在于,它会在不读取整个文件的情况下开始产生,这就是您想要的行为。