从异步链接Observable时的处理

时间:2018-01-25 06:22:33

标签: c# system.reactive

假设我有以下Observable。 (请注意,解析逻辑位于不同的层中,并且应该是可测试的,因此它必须保持单独的方法。还要注意实际循环是解析XML并具有各种分支和异常处理)。

IObservable<string> GetLinesAsync(StreamReader r)
{
    return Observable.Create<string>(subscribeAsync: async (observer, ct) =>
    {
        //essentially force a continuation/callback to illustrate my problem
        await Task.Delay(5);

        while (!ct.IsCancellationRequested)
        {
            string readLine = await r.ReadLineAsync();
            if (readLine == null)
                break;

            observer.OnNext(readLine);
        }
    });
}

我想使用此功能,例如使用另一个产生Observable的{​​{1}},如下所示,但无论如何我都无法处理。

StreamReader

如果你运行这几次(例如使用断点等),你应该会看到它因为TextReader关闭而爆炸。 (在我的实际问题中,它偶尔会在[TestMethod] public async Task ReactiveTest() { var filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "win.ini"); var source1 = Observable.Using( () => File.OpenRead(filePath), readFile => Observable.Using(() => new StreamReader(readFile), reader => Observable.Return(reader) ) ); //"GetLinesAsync" already exists. How can I use it? var combined = source1 .SelectMany(GetLinesAsync); int count = await combined.Count(); } 上发生,但ReadLineAsync会让它更容易发生。显然,异步性质会导致第一个observable处理流,并且只有在此之后才会发生延续,当然此时流已经关闭。

所以:

  1. 是第一个使用设置正确的一次性用品吗?我尝试了其他方式(见下文*)
  2. 这是执行异步Observable(即GetLinesAsync)的正确方法吗?还有什么我需要做的吗?
  3. 这是将观察者链接在一起的正确方法吗?假设Task.Delay已经存在,如果可能,则不应更改其签名(例如,接收GetLinesAsync
  4. 如果这是将观察者粘合在一起的正确方法,有没有办法让它与IObservable<StreamReader>一起使用?
  5. *这是我设置第一个可观察的

    的另一种方式
    async

3 个答案:

答案 0 :(得分:2)

您真的需要在这里充分利用DeferUsing运算符。

Using专门针对您希望创建并在订阅开始和完成时最终处理的可支配资源的情况。

Defer是一种确保您在新订阅时始终创建新管道的方法(read more on MSDN

你的第二种方法是要走的路。你有100%的权利:

Observable.Using(
    () => File.OpenRead(filePath),
    readFile =>
        Observable.Using(
            () => new StreamReader(readFile),
            reader =>

这将在每个正确的时间打开并处理资源。

这是代码块之前的内容以及需要修复的reader =>之后的内容。

reader =>之后:

Observable
    .Defer(() => Observable.FromAsync(() => reader.ReadLineAsync()))
    .Repeat()
    .TakeWhile(x => x != null)));

这是Rx从流中读取直到完成的惯用方法。

“之前”块只是另一个Defer,以确保您为每个新订阅者计算Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "win.ini")。在这种情况下没有必要,因为我们知道filePath不会改变,但这是一种很好的做法,而且当这个值发生变化时非常重要。

这是完整的代码:

public async Task ReactiveTest()
{
    IObservable<string> combined =
        Observable.Defer(() =>
        {
            var filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "win.ini");
            return
                Observable.Using(
                    () => File.OpenRead(filePath),
                    readFile =>
                        Observable.Using(
                            () => new StreamReader(readFile),
                            reader =>
                                Observable
                                    .Defer(() => Observable.FromAsync(() => reader.ReadLineAsync()))
                                    .Repeat()
                                    .TakeWhile(x => x != null)));
        });

    int count = await combined.Count();
}

我已经对它进行了测试,效果非常好。

鉴于你有GetLines的固定签名,你可以这样做:

public IObservable<string> GetLines(StreamReader reader)
{
    return Observable
        .Defer(() => Observable.FromAsync(() => reader.ReadLineAsync()))
        .Repeat()
        .TakeWhile(x => x != null);
}

public async Task ReactiveTest()
{
    IObservable<string> combined =
        Observable.Defer(() =>
        {
            var filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "win.ini");
            return
                Observable.Using(
                    () => File.OpenRead(filePath),
                    readFile =>
                        Observable.Using(
                            () => new StreamReader(readFile),
                            GetLines));
        });

    int count = await combined.Count();
}

它也有效并经过测试。

答案 1 :(得分:1)

您遇到的问题是您的序列会返回一个项目,即读者。利用阅读器,需要打开文件流。遗憾的是,文件流在创建流阅读器后立即关闭:

  1. StreamReader reader已创建
  2. OnNext(reader)被称为
  3. using阻止退出,处理流
  4. 调用OnComplete,终止订阅
  5. 糟糕!

    要解决此问题,您必须将StreamReader的生命周期与使用者的生命周期联系起来,而不是生产者。发生原始错误是因为Observable.Using在源上调用OnCompleted后立即处置资源。

    // Do not dispose of the reader when it is created
    var readerSequence = Observable.Return(new StreamReader(ms));
    
    var combined = readerSequence
        .Select(reader =>
        {
            return Observable.Using(() => reader, resource => GetLines(resource));
        })
        .Concat();
    

    我不是一个忠实的粉丝,因为你现在依靠你的消费者来清理每个StreamReader,但我还没有制定更好的方法!

答案 2 :(得分:0)

到目前为止,这是允许我继续使用GetLinesAsync的唯一有效的方法:

//"GetLinesAsync" already exists.  How can I use it?


[TestMethod]
public async Task ReactiveTest2()
{
    var combined2 = Observable.Create<string>(async observer =>
    {
        var filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "win.ini");

        using (FileStream readFile = File.OpenRead(filePath))
        {
            using (StreamReader reader = new StreamReader(readFile))
            {
                await GetLinesAsync(reader)
                    .ForEachAsync(result => observer.OnNext(result));
            }
        }
    });


    int count = await combined2.Count();
}

这不能可靠地运作:

[TestMethod]
public async Task ReactiveTest3()
{
    var filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "win.ini");

    var source1 = Observable.Defer(() => Observable.Using(
        () => File.OpenRead(filePath),
        readFile => Observable.Using(() => new StreamReader(readFile),
            reader => Observable.Return(reader)
        )
    ));

    //"GetLines" already exists.  How can I use it?
    var combined = source1      
            .SelectMany(reader => Observable.Defer(() => GetLinesAsync(reader)));

    int count = await combined.Count();

}

根据Enigmativity的解决方案,只有一个Observable似乎才有效:

[TestMethod]
public async Task ReactiveTest4()
{
    var filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "win.ini");

    var source1 = Observable.Using(
        () => File.OpenRead(filePath),
        readFile => Observable.Using(() => new StreamReader(readFile),
            reader => GetLinesAsync(reader)
        )
    );

    int count = await source1.Count();

}

但是我还没有找到一种方法来保持两个Observable(我用于分层和单元测试目的)之间的分离并让它正常工作,所以我不会考虑回答这个问题。