订阅在Reactive Extensions中结束时关闭非托管资源

时间:2015-04-24 12:21:27

标签: c# system.reactive

我正在从Rx向网络写入数据。当然,我在订阅结束时使用Finally来关闭我的流。这可以在OnError()OnComplete()上干净利落地运行。 Rx将按顺序运行OnNext() ... OnNext()OnComplete()Finally()

但是,有时我想提前终止序列,为此我使用Dispose()。不知怎的,Finally()现在与上一次OnNext()调用并行运行,导致在OnNext()中仍然写入流时出现异常,以及写入不完整。

我的订阅大致如下:

NetworkStream stm = client.GetStream();
IDisposable disp = obs
    .Finally(() => {
        client.Close();
    })
    .Subscribe(d => {
        client.GetStream().Write(d.a, 0, d.a.Lenght);
        client.GetStream().Write(d.b, 0, d.b.Lenght);
    } () => {
        client.GetStream().Write(something(), 0, 1);
    });
Thread.sleep(1000);
disp.Dispose();

我也尝试过替代方案,CancellationToken

如何正确取消订阅?我不介意它跳过OnComplete(),只要Finally()仍在运行。但是,并行运行Finally()会有问题。

我也觉得应该有更好的方法来管理资源,将声明移到序列中,这将是一个更好的解决方案。

编辑:以下代码更清楚地显示问题。我希望它总是打印出来,相反,它会经常给出错误,指示Dispose在最后OnNext之前结束。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Try finally");
            for (int i = 0; i < 10; i++)
            {
                Finally();
            }
            Console.WriteLine("Try using");
            for (int i = 0; i < 10; i++)
            {
                Using();
            }
            Console.WriteLine("Try using2");
            for (int i = 0; i < 10; i++)
            {
                Using2();
            }
            Console.ReadKey();
        }

        private static void Using2()
        {
            bool b = true, c = true, d;
            var dis = Disposable.Create(() => c = b);
            IDisposable obDis = Observable.Using(
                () => dis,
                _ => Observable.Create<Unit>(obs=>
                    Observable.Generate(0,
                    i => i < 1000,
                    i => i + 1,
                    i => i,
                    i => TimeSpan.FromMilliseconds(1)
                ).Subscribe(__ => { b = false; Thread.Sleep(100); b = true; })))
                .Subscribe();
            Thread.Sleep(15);
            obDis.Dispose();
            d = b;
            Thread.Sleep(101);
            Console.WriteLine("OnDispose: {1,5} After: {2,5} Sleep: {0,5}", b, c, d);
        }

        private static void Using()
        {
            bool b = true, c = true, d;
            var dis = Disposable.Create(() => c = b);
            IDisposable obDis = Observable.Using(
                () => dis,
                _ => Observable.Generate(0,
                    i => i < 1000,
                    i => i + 1,
                    i => i,
                    i => TimeSpan.FromMilliseconds(1)
                )).Subscribe(_ => { b = false; Thread.Sleep(100); b = true; });
            Thread.Sleep(15);
            obDis.Dispose();
            d = b;
            Thread.Sleep(101);
            Console.WriteLine("OnDispose: {1,5} After: {2,5} Sleep: {0,5}", b, c, d);
        }

        private static void Finally()
        {
            bool b = true, c = true, d;
            IDisposable obDis = Observable.Generate(0,
                i => i < 1000,
                i => i + 1,
                i => i,
                _ => DateTime.Now.AddMilliseconds(1)
                )
                .Finally(() => c = b)
                .Subscribe(_ => { b = false; Thread.Sleep(100); b = true; });
            Thread.Sleep(15);
            obDis.Dispose();
            d = b;
            Thread.Sleep(101);
            Console.WriteLine("OnDispose: {1,5} After: {2,5} Sleep: {0,5}", b, c, d);
        }
    }
}

2 个答案:

答案 0 :(得分:5)

Finally很可能不是你想要的。取消订阅时,它不会处置您的资源。相反,它的行为类似于C#中的普通finally块,也就是说,它将保证执行某些代码,无论相应的try - 块中的代码是否引发异常。此外,给定this question on MSDNFinally中的代码甚至可能无法在每种情况下执行,因为您的订阅未指定错误处理程序。

您可能想要的是Using

IDisposable disp = Observable
    .Using(
        () => Disposable.Create(() => client.Close),
        _ => obs)
    .Subscribe(....);
只要observable终止或订阅被取消,

Using就会小心处理资源。

假设client是TcpClient,它变得更简单:

IDisposable disp = Observable
    .Using(
        () => client),
        _ => obs)
    .Subscribe(....);

我希望对OnNext的调用不会与关闭客户端重叠,即使提前取消预订,但我还没有对此进行测试。

最后一件事:注意在示例中关闭外部变量,例如stm。总是和当地人一起工作更安全。正如我试试的那样完整的重写是这样的:

IDisposable disp = Observable.Using(
    () => client,
    _ => Observable.Using(
         () => client.GetStream(),
         stream => Observable.Create<Unit>(observer => obs
             .Subscribe(
                 d => {
                     stream.Write(d.a, 0, d.a.Lenght);
                     stream.Write(d.b, 0, d.b.Lenght);
                 },
                 () => {
                     stream.Write(something(), 0, 1);
                 }))))
    .Subscribe();

答案 1 :(得分:0)

我认为您对NetworkStream的运作方式做了一个不正确的假设。

NetworkStream.WriteTcpClient.Close不一定等待客户端读取数据。 (此外,NetworkStream.Flush没有做任何事情。)

当您致电Close时,您可能在客户端已阅读所有内容之前关闭了套接字。

查看此相关问题:NetworkStream doesn't always send data unless I Thread.Sleep() before closing the NetworkStream. What am I doing wrong?

该页面不同地提及诸如使用接受超时的Close重载或指定LingerOption之类的事情 - 但最好是发送Shutdown或更高级别的消息抽象,客户通过自己的回复确认您的消息。