没有最终订阅者的“中间IObservable”会在根IObservable的生命周期内保留在内存中

时间:2012-03-16 12:58:52

标签: c# .net system.reactive idisposable

例如,考虑一下:

    public IDisposable Subscribe<T>(IObserver<T> observer)
    {
        return eventStream.Where(e => e is T).Cast<T>().Subscribe(observer);
    }

eventStream是一个长期存在的事件来源。短期客户端将使用此方法订阅一段时间,然后通过在返回的Dispose上调用IDisposable取消订阅。

但是,虽然eventStream仍然存在且应保留在内存中,但此方法创建了2个新IObservables - Where()方法返回的方法可能是由eventStream保存在内存中,Cast<T>()方法返回的方法可能由Where()方法返回的方法保存在内存中。

这些'中间IObservables'(它们有更好的名字吗?)如何清理?或者它们现在会在eventStream的生命周期中存在,即使它们不再有订阅而没有其他人引用它们,除了它们的来源IObservable,因此永远不会再有订阅?

如果通过通知他们的父母他们不再有订阅来清理他们,他们怎么知道没有其他人参考他们并且可能在某些时候后来订阅他们?

5 个答案:

答案 0 :(得分:2)

  

然而,虽然eventStream仍然存在并且应该保存在内存中,但是这个方法创建了2个新的IObservable - 由eventStream可能在内存中保存的Where()方法返回的IObservable,以及由Cast()方法返回,该方法可能由Where()方法返回的方法保存在内存中。

你有这个落后。让我们来看看正在发生的事情。

IObservable<T> eventStream; //you have this defined and assigned somewhere

public IDisposable Subscribe<T>(IObserver<T> observer)
{
    //let's break this method into multiple lines

    IObservable<T> whereObs = eventStream.Where(e => e is T);
    //whereObs now has a reference to eventStream (and thus will keep it alive), 
    //but eventStream knows nothing of whereObs (thus whereObs will not be kept alive by eventStream)
    IObservable<T> castObs = whereObs.Cast<T>();
    //as with whereObs, castObs has a reference to whereObs,
    //but no one has a reference to castObs
    IDisposable ret = castObs.Subscribe(observer);
    //here is where it gets tricky.
    return ret;
}

ret有或没有引用的内容取决于各种observable的实现。根据我在Rx库中的Reflector中看到的以及我自己编写的运算符,大多数运算符都不会返回对运算符本身具有引用的一次性用法。

例如,Where的基本实现就像(直接在编辑器中输入,没有错误处理)

IObservable<T> Where<T>(this IObservable<T> source, Func<T, bool> filter)
{
    return Observable.Create<T>(obs =>
      {
         return source.Subscribe(v => if (filter(v)) obs.OnNext(v),
                                 obs.OnError, obs.OnCompleted);
      }
}

请注意,返回的一次性将通过创建的观察者引用过滤器函数,但不会引用Where observable。 Cast可以使用相同的模式轻松实现。实质上,操作员成为观察者包装工厂。

所有这些对手头问题的影响是,中间IObservable在方法结束时符合垃圾收集的条件。传递给Where的过滤器函数只要订阅就会保留,但是一旦处理或完成订阅,只剩下eventStream(假设它仍然存在)。

对于supercat的评论,

编辑,让我们看看编译器如何重写这个或如何在没有闭包的情况下实现它。

class WhereObserver<T> : IObserver<T>
{
    WhereObserver<T>(IObserver<T> base, Func<T, bool> filter)
    {
        _base = base;
        _filter = filter;
    }

    IObserver<T> _base;
    Func<T, bool> _filter;

    void OnNext(T value)
    {
        if (filter(value)) _base.OnNext(value);
    }

    void OnError(Exception ex) { _base.OnError(ex); }
    void OnCompleted() { _base.OnCompleted(); }
}

class WhereObservable<T> : IObservable<T>
{
    WhereObservable<T>(IObservable<T> source, Func<T, bool> filter)
    {
        _source = source;
        _filter = filter;
    }

    IObservable<T> source;
    Func<T, bool> filter;

    IDisposable Subscribe(IObserver<T> observer)
    {
        return source.Subscribe(new WhereObserver<T>(observer, filter));
    }
}

static IObservable<T> Where(this IObservable<T> source, Func<T, bool> filter)
{
    return new WhereObservable(source, filter);
}

您可以看到观察者不需要对生成它的observable的任何引用,并且observable不需要跟踪它创建的观察者。我们甚至没有从订阅中返回任何新的IDisposable。

实际上,Rx有一些匿名observable / observer的实际类,它们接受委托并将接口调用转发给那些委托。它使用闭包来创建这些委托。编译器不需要发出实际实现接口的类,但翻译的精神保持不变。

答案 1 :(得分:1)

我想我已经在基甸的答案的帮助下得出结论并打破了样本Where方法:

我错误地认为上游始终引用了每个下游IObservable(为了在需要时推送事件)。但是这会在上游的生命周期中将下游存储在内存中。

实际上,下游IObservable引用了每个上游IObservable(等待,准备好在需要时挂钩IObserver)。只要引用了下游,这就会在内存中生成上游(这是有道理的,因为在某个地方仍然引用了下游,但订阅可能随时发生)。

但是,当订阅确实发生时,此上游到下游引用链确实会形成,但仅限于在每个可观察阶段管理订阅的IDisposable实现对象,并且仅在该订阅的生命周期内。 (这也是有意义的 - 当订阅存在时,每个上游'处理逻辑'仍然必须保存在内存中以处理传递到最终订阅者IObserver的事件。)

这给出了两个问题的解决方案 - 当引用IObservable时,它将把所有源(上游)IObservables保存在内存中,为订阅做好准备。虽然订阅存在,但它会将所有下游订阅保留在内存中,允许最终订阅仍然接收事件,即使它的源IObservable可能不再被引用。

在我的问题中将此应用于我的示例,WhereCast下游可观察量非常短暂 - 在Subscribe(observer)调用完成之前引用。然后他们可以自由收集。现在可以收集中间可观察量的这一事实不会对刚刚创建的订阅造成问题,因为它已经形成了它自己的订阅对象链(上游 - >下游),其源自源eventStream可观察对象。一旦每个下游阶段处置其IDisposable订阅跟踪器,该链将被释放。

答案 2 :(得分:1)

您需要记住IObserable<T>(如IEnumerable<T>)是惰性列表。在有人试图通过订阅或迭代来访问元素之前,它们不存在。

当您编写list.Where(x => x > 0)时,您没有创建新列表,您只是定义了如果有人试图访问这些元素,新列表会是什么样子。

这是一个非常重要的区别。

您可以认为有两种不同的IObservable。一个是定义和订阅实例。

IObservable定义旁边没有内存使用。参考文献可以自由分享。他们将被干净地收集垃圾。

订阅的实例仅在订阅了某人时才存在。他们可能会使用大量的记忆除非您使用.Publish扩展名,否则无法共享引用。当订阅结束或通过调用.Dispose()终止时,内存将被清除。

为每个新订阅创建一组新的订阅实例。当最终子订阅被处置时,整个链被处置。他们无法分享。如果存在第二个订阅,则创建完整的订阅实例链,与第一个订阅实例无关。

我希望这会有所帮助。

答案 3 :(得分:0)

实现IObservable的类只是一个常规对象。 GC运行时会清理它并且看不到任何引用。除了“new object()何时清理”之外,它不是什么。除了使用内存之外,您的程序不应该看到它们是否被清理。

答案 4 :(得分:0)

如果一个对象订阅了事件,无论是为了自己使用,还是为了将它们转发给其他对象,那么这些事件的发布者通常会保持活着,即使没有其他人会这样做。如果我正确理解您的情况,那么您拥有订阅事件的对象,以便将它们转发给零个或多个其他订阅者。我建议你尽可能设计你的中间IObservable,这样他们就不会订阅他们父母的活动,直到有人订阅他们的活动,他们将在他们的最后一个订阅者取消订阅的时候取消订阅他们的父活动。这是否实用将取决于父母和子女IObservables的线程上下文。进一步注意,(再次取决于线程上下文)可能需要锁定来处理新订户在最后一个订户退出的同时加入的情况。尽管大多数对象的订阅和取消订阅方案都可以使用CompareExchange而不是锁定来处理,但在涉及互连订阅列表的情况下,这通常是行不通的。

如果您的对象将在线程上下文中接收其子节点的订阅和取消订阅,这与父节点的订阅和取消订阅方法不兼容(恕我直言,IObservable应该要求所有合法实现允许任意订阅和取消订阅线程上下文,但唉它没有)你可能别无选择,只需要在创建时立即创建一个代理对象来代表你处理订阅,并让该对象订阅父事件。然后拥有自己的对象(代理只有弱引用)包括一个终结器,它将通知代理在其父级的线程上下文允许时需要取消订阅。在最后一个订阅者退出时让你的代理对象取消订阅会很好,但如果新订阅者可能加入并期望其订阅立即生效,只要有人持有对中间人的引用,就可能必须保留代理订阅。可用于请求新订阅的观察者。