Do 'Intermediate IObservables' without final subscribers get kept in memory for the lifetime of the root IObservable

与世无争的帅哥 提交于 2020-01-14 09:59:10

问题


For example, consider this:

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

The eventStream is a long lived source of events. A short lived client will use this method to subscribe for some period of time, and then unsubscribe by calling Dispose on the returned IDisposable.

However, while the eventStream still exists and should be kept in memory, there has been 2 new IObservables created by this method - the one returned by the Where() method that is presumably held in memory by the eventStream, and the one returned by the Cast<T>() method that is presumably held in memory by the one returned by the Where() method.

How will these 'intermediate IObservables' (is there a better name for them?) get cleaned up? Or will they now exist for the lifetime of the eventStream even though they no longer have subscriptions and no one else references them except for their source IObservable and therefor will never have subscriptions again?

If they are cleaned up by informing their parent they no longer have subscriptions, how do they know nothing else has taken a reference to them and may at some point later subscribe to them?


回答1:


However, while the eventStream still exists and should be kept in memory, there has been 2 new IObservables created by this method - the one returned by the Where() method that is presumably held in memory by the eventStream, and the one returned by the Cast() method that is presumably held in memory by the one returned by the Where() method.

You have this backward. Let's walk through the chain of what is going on.

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;
}

What ret does or does not have a reference to depends on the implementation of the various observables. From what I have seen in Reflector in the Rx library and the operators I have written myself, most operators do not return disposables that have a reference to the operator observable itself.

For example, a basic implementation of Where would be something like (typed directly in the editor, no error handling)

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);
      }
}

Notice that the disposable returned will have a reference to the filter function via the observer that is created, but will not have a reference to the Where observable. Cast can be easily implemented using the same pattern. In essence, the operators become observer wrapper factories.

The implication of all this to the question at hand is that the intermediate IObservables are eligible for garbage collection by the end of the method. The filter function passed to Where stays around as long as the subscription does, but once the subscription is disposed or completed, only eventStream remains (assuming it is still alive).

EDIT for supercat's comment, let's look at how the compiler might rewrite this or how you would implement this without closures.

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);
}

You can see that the observer does not need any reference to the observable that generated it and the observable has no need to track the observers it creates. We didn't even make any new IDisposable to return from our subscribe.

In reality, Rx has some actual classes for anonymous observable/observer that take delegates and forward the interface calls to those delegates. It uses closures to create those delegates. The compiler does not need to emit classes that actually implement the interfaces, but the spirit of the translation remains the same.




回答2:


I think I've come to the conclusion with the help of Gideon's answer and breaking down a sample Where method:

I assumed incorrectly that each downstream IObservable was referenced by the upstream at all times (in order to push events down when needed). But this would root downstreams in memory for the lifetime of the upstream.

In fact, each upstream IObservable is referenced by the downstream IObservable (waiting, ready to hook an IObserver when required). This roots upstreams in memory as long as the downstream is referenced (which makes sense, as while a downstream in still referenced somewhere, a subscription may occur at any time).

However when a subscription does occur, this upstream to downstream reference chain does get formed, but only on the IDisposable implementation objects that manage the subscriptions at each observable stage, and only for the lifetime of that subscription. (which also makes sense - while a subscription exists, each upstream 'processing logic' must still be held in memory to handle the events being passed through to reach the final subscriber IObserver).

This gives a solution to both problems - while an IObservable is referenced, it will hold all source (upstream) IObservables in memory, ready for a subscription. And while a subscription exists, it will hold all downstream subscriptions in memory, allowing the final subscription to still receive events even though it's source IObservable may no longer be referenced.

Applying this to my example in my question, the Where and Cast downstream observables are very short lived - referenced up until the Subscribe(observer) call completes. They are then free to be collected. The fact that the intermediate observables may now be collected does not cause a problem for the subscription just created, as it has formed it's own subscription object chain (upstream -> downstream) that is rooted by the source eventStream observable. This chain will be released as soon as each downstream stage disposes its IDisposable subscription tracker.




回答3:


You need to remember that IObserable<T> (like IEnumerable<T>) are lazy lists. They don't exist until someone tries to access the elements by subscribing or iterating.

When you write list.Where(x => x > 0) you are not creating a new list, you are merely defining what the new list will look like if someone tries to access the elements.

This is a very important distinction.

You can consider that there are two different IObservables. One is the definition and the subscribed instances.

The IObservable definitions use next to no memory. References can be freely shared. They will be cleanly garbage collected.

The subscribed instances only exist if someone is subscribed. They may use considerable memory. Unless you use the .Publish extensions you can't share references. When the subscription ends or is terminated by calling .Dispose() the memory is cleaned up.

A new set of subscribed instances are created for every new subscription. When the final child subscription is disposed the whole chain is disposed. They can't be shared. If there is a second subscription a complete chain of subscribed instances are created, independent of the first.

I hope this helps.




回答4:


A class implementing IObservable is just a regular object. It will get cleaned up when the GC runs and does not see any references to it. It isn't anything other than "when does new object() get cleaned up". Except for memory use, whether they get cleaned up should not be visible to your program.




回答5:


If an object subscribes to events, whether for its own use, or for the purpose of forwarding them to other objects, the publisher of those events will generally keep it alive even if nobody else will. If I'm understanding your situation correctly, you have objects which subscribe to events for the purpose of forwarding them to zero or more other subscribers. I would suggest that you should if possible design your intermediate IObservables so that they will not subscribe to an event from their parent until someone subscribes to an event from them, and they will unsubscribe from their parent's event any time their last subscriber unsubscribes. Whether or not this is practical will depend upon the threading contexts of the parent and child IObservables. Further note that (again depending upon threading context) locking may be required to deal with the case where a new subscriber joins at about the same time as (what would have been) the last subscriber quits. Even though most objects' subscription and unsubscription scenarios could be handled using CompareExchange rather than locking, that is often unworkable in scenarios involving interconnected subscription lists.

If your object will receive subscriptions and unsubscriptions from its children in a threading context which is not compatible with the parent's subscription and unsubscription methods (IMHO, IObservable should have required that all legitimate implementations allow subscription and unsubscription from arbitrary threading context, but alas it does not) you may have no choice but to have the intermediate IObservable, immediately upon creation, create a proxy object to handle subscriptions on your behalf, and have that object subscribe to the parent's event. Then have your own object (to which the proxy would have only a weak reference) include a finalizer which will notify the proxy that it will need to unsubscribe when its parent's threading context permits. It would be nice to have your proxy object unsubscribe when its last subscriber quits, but if a new subscriber might join and expect its subscription to be valid immediately, one may have to keep the proxy subscribed as long as anyone holds a reference to the intermediate observer which could be used to request a new subscription.



来源:https://stackoverflow.com/questions/9737711/do-intermediate-iobservables-without-final-subscribers-get-kept-in-memory-for

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!