Reactive-Streams API(三):资源管理与 TakeUntil

916 阅读13分钟
原文链接: blog.piasy.com
本文是 Advanced RxJava http://akarnokd.blogspot.com/ 系列博客的中文翻译,已征得作者授权。该系列博客的作者是 RxJava 的核心贡献者之一。翻译的内容使用 知识共享 署名-非商业性使用-相同方式共享 4.0 国际 协议进行许可,转载请注明出处。如果发现翻译问题,或者任何改进意见,请 在 Github 上提交 issue 。 本文是 Piasy 独立翻译,发表于 blog.piasy.com/AdvancedRxJ…,请阅读原文支持原创 blog.piasy.com/AdvancedRxJ…

原文 The Reactive-Streams API (part 3)

介绍

在本文中,我将讲解如何把 rx.Subscriber 管理资源的能力移植到 Reactive-Streams API 中。但是由于 RS 并未明确任何资源管理方面的要求,所以我们需要引入(把 rx.Subscription 重命名)我们自己的容器类型,并把它加入到 RS 的 Subscriber 的取消逻辑中。

为了避免造成困惑,RxJava 2.0 把 XXXSubscription 替换为了 XXXDisposable,我不会在这里详细介绍这些类,但是会讲几个资源管理的基本接口:

interface Disposable {
    boolean isDisposed();
    void dispose();
}
 
interface DisposableCollection extends Disposable {
    boolean add(Disposable resource);
    boolean remove(Disposable resource);
    boolean removeSilently(Disposable resource);
 
    void clear();
    boolean hasDisposables();
    boolean contains(Disposable resource);
}

使用的规则是一样的:线程安全、幂等性。

加入资源管理最基本的方式就是对 Subscription 进行一次包装,拦截 cancel() 调用并且调用底层容器类的 dispose()

public final class DisposableSubscription
implements Disposable, Subscription {
    final Subscription actual;
    final DisposableCollection collection;
    public DisposableSubscription(
            Subscription actual, 
            DisposableCollection collection) {
        this.actual = Objects.requireNonNull(actual);
        this.collection = Objects.requireNonNull(collection);
    }
    public boolean add(Disposable resource) {
        return collection.add(resource);
    }
    public boolean remove(Disposable resource) {
        return collection.remove(resource);
    }
    public boolean removeSilently(Disposable resource) {
        return collection.remove(resource);
    }
    @Override
    public boolean isDisposed() {
        return collection.isDisposed();
    }
    @Override
    public void dispose() {
        cancel();
    }
     
    @Override
    public void cancel() {
        collection.dispose();
        actual.cancel();
    }
    @Override
    public void request(long n) {
        actual.request(n);
    }
}

由于 DisposableSubscription 也实现了 Disposable 接口,所以它也可以被加入到容器中,构成一个复杂的 dispose 网络。但是绝大多数情况下,我们都希望避免额外的内存分配,因此,上面的这些代码可能会被融入到其他的类型中,例如在 lift() 调用中创建的 Subscriber 类型。

如果你熟悉 RxJava 的规范,以及操作符实现的陷阱之一,那就不应该取消订阅下游,因为这可能会导致资源的提前释放。

这也是目前 RxAndroid 的 LifecycleObservable 的一个 bug,当我们在中间插入一个类似于 takeUntil() 的操作符时,它不会向下游发送 onCompleted(),而是取消订阅了下游。

在 RS 中,取消订阅下游实际上是不会发生的。每一层都要么只能原封不动的转发 Subscription(因此无法添加资源),要么只能把它包装为 DisposableSubscription 这样的类型,然后依然把下游当做一个 Subscription 进行转发。如果你在这一层调用了 cancel(),你是无法调用包装 Subscriber 的类的 cancel() 的。

当然,你非要搞破坏那肯定是可以的,但 RS 比 RxJava 做了更多的努力,而且原则是不变的:不应该 cancel/dispose 下游的资源,或者在操作符链条中共享资源。

TakeUntil

现在让我们看看怎么实现能够管理外部资源的 takeUntil() 操作符(我把代码拆分了一下,更方便阅读):

public final class OperatorTakeUntil 
implements Operator {
    final Publisher other;
    public OperatorTakeUntil(Publisher other) {
        this.other = Objects.requireNonNull(other);
    }
    @Override
    public Subscriber super T> call(
            Subscriber super T> child) {
        Subscriber serial = 
            new SerializedSubscriber<>(child);
 
        SubscriptionArbiter arbiter = 
            new SubscriptionArbiter();                       // (1)
        serial.onSubscribe(arbiter);
         
        SerialDisposable sdUntil = new SerialDisposable();   // (2)
        SerialDisposable sdParent = new SerialDisposable();  // (3)

到目前为止,看起来都和 RxJava 的实现类似:我们把 child 包装为一个 SerializedSubscriber,防止 Publisher 并行发出 onError()onCompleted()

我们创建了一个 SubscriptionArbiter(一个 ProducerArbiter 的变体),主要是考虑以下原因:假设我们正在 call() 函数中,另一个已经订阅的数据源发出了一个数据,那我们就需要把数据转发到 child Subscriber 中,然而在我们得到 Subscription 之前(调用 onSubscribe() 之前),我们是无法在其上调用 onXXX 函数的,所以我们只能等到操作符链条上调用 onSubscribe() 之后(拿到 Subscription 之后),才可以转发数据。我会在下一篇文章中更详细地讲解这个问题。

然而,由于取消订阅的机会在 Subscriber 那里(我们调用 Subscription.cancel() 取消订阅,而我们会调用 Subscriber.onSubscribe(Subscription)Subscription 交给 Subscriber),我们需要把 Subscription 从子 Subscriber 传递到父 Subscriber 中(2),这样它们中的任意一个到达终止状态时,它都能 cancel() 另一个。由于子 Subscriber 可能比父 Subscriber 先收到 Subscription 和取消事件,我们也将需要反过来取消父 Subscriber(3)。

// ...
Subscriber parent = new Subscriber() {
    DisposableSubscription dsub;
    @Override
    public void onSubscribe(Subscription s) {
        DisposableSubscription dsub = 
                new DisposableSubscription(s, 
                new DisposableList());              // (1)
        dsub.add(sdUntil);                          // (2)
        sdParent.set(dsub);
        arbiter.setSubscription(dsub);              // (3)
    }
    @Override
    public void onNext(T t) {
        serial.onNext(t);
    }
    @Override
    public void onError(Throwable t) {
        serial.onError(t);
        sdParent.cancel();                          // (4)
    }
    @Override
    public void onComplete() {
        serial.onComplete();
        sdParent.cancel();
    }
};

父 Subscriber 的实现略有不同,我们需要处理后来的 Subscription,并且建立好取消订阅的链条:

  1. 我们创建了一个 DisposableSubscription,底层使用基于 List 的 Disposable 集合。
  2. 我们把包装了 Disposable(指向另一个 Subscription) 的 SerialDisposable 加入到容器中。我们也把 DisposableSubscription 加入到 sdParent 中,这就让另一个 Subscriber 可以在 parent 开始之前结束自己。
  3. 我们把包装好的对象加入到 arbiter 中。
  4. 当错误事件发生时,我们要确保取消掉容器。而由于容器中包含了另一个 Subscription,所以整个事件流也会被取消。

最后我们需要为另一个事件流创建 Subscriber,并且确保它和 parent 连接起来:

        // ...
        Subscriber until = new Subscriber() {
            @Override
            public void onSubscribe(Subscription s) {
                sdUntil.set(Disposables.create(s::cancel));    // (1)
                s.request(Long.MAX_VALUE);
            }
            @Override
            public void onNext(Object t) {
                parent.onComplete();                           // (2)
            }
            @Override
            public void onError(Throwable t) {
                parent.onError(t);
            }
            @Override
            public void onComplete() {
                parent.onComplete();
            }
        };
         
        this.other.subscribe(until);
         
        return parent;
    }
}

我们从另一个数据源接收到 Subscription 时,我们就把它包装为一个 Disposable(和现在 Subscription.create() 的做法一样)。由于 Disposable 的终结状态特性,即便我们的主要事件流在另一个流接收到 Subscription 之前就已经结束,包装的 SerialDisposable 依然会被取消,而这就会立即取消刚刚接收到的 Subscription。(译者注:对于“终结状态特性”,不了解的朋友可以看看之前的文章:Operator 并发原语: subscription-containers(一)

注意,由于取消/释放资源的能力取决于时间和顺序,所以我们通常都需要为每个资源创建一个容器(例如 sdParentsdOther),这样无论哪个 Subscription 在任何时候到达时,我们都能释放所有的资源。

TakeUntil v2

如果仔细看看上面 takeUntil() 的实现,就会发现我们对各种 Subscription 进行了重新组织,我们其实可以理清 Disposable 导致的混乱:

@Override
// ...
public Subscriber super T> call(Subscriber super T> child) {
    Subscriber serial = new SerializedSubscriber<>(child);
 
    SubscriptionArbiter sa = new SubscriptionArbiter();        // (1)
 
    DisposableSubscription dsub = 
        new DisposableSubscription(sa, new DisposableList());  // (2)
     
    serial.onSubscribe(dsub);                                  // (3)
     
    Subscriber parent = new Subscriber() {
        @Override
        public void onSubscribe(Subscription s) {
            dsub.add(Disposables.create(s::cancel));           // (4)
            sa.setSubscription(s);                             // (5)
        }
        // ...
    };
     
    Subscriber until = 
    new Subscriber() {
        @Override
        public void onSubscribe(Subscription s) {
            dsub.add(Disposables.create(s::cancel));           // (6)
            s.request(Long.MAX_VALUE);
        }
        // ...

它的原理如下:

  1. 我们创建了一个 SubscriptionArbiter
  2. 然后把它包装为一个 DisposableSubscription
  3. 然后把它推到下游。这种组合能保证任何的取消以及请求,都会积累到 arbiter 接收到一个“真正的” Subscription 时。
  4. 一旦主流收到 Subscription 之后,我们就把它包装为一个 Disposable 并加入到 dsub 容器中。
  5. 然后我们就更新 arbiter 的 Subscription:所有积累的请求以及取消操作都会“重放”到上游的 Subscription 上。
  6. 当另一条流收到 Subscription 之后,我们也把它包装为一个 Disposable 并加入到 dsub 容器中。

最后 parent 会在它的 onError()onComplete() 中调用 dsub.dispose() 了。

让我们梳理一下各种取消的路径:

  • 下游取消:下游会取消 dsubdsub 会取消 arbiter,arbiter 会取消任何收到的 Subscription
  • 主流结束:主流会取消 dsubdsub 会取消 arbiter,arbiter 会取消上游的 Subscription。此外,dsub 也会取消其他存在的 Subscription
  • 支流结束:支流会取消 dsubdsub 会取消 arbiter。一旦主流收到 Subscription 之后,dsub 和 arbiter 都会立即取消这个 Subscription

总结

在本文中,我讲解了怎么在 Reactive-Stream 的 SubscriberSubscription 体系中实现资源管理,以及演示了如何实现一个 takeUntil() 操作符。

尽管看起来我们创建了和 RxJava 实现中同样多(甚至更多)的对象,但是很多操作符都不需要资源管理(例如 take()),甚至都不需要包装收到的 Subscription 对象(例如 map())。

在下一篇(最后一篇)关于 RS API 的文章中,我将介绍我们对各种 arbiter 类型更大的需求(在本文的 takeUntil() 例子中已经有所涉及),因为我们必须给 Subscriber 设置一个 Subscription 并且还要保证取消功能的正常,此外即便数据源发生了变化我们也不能调用多次 onSubscribe()

欢迎大家关注我的微信公众号,会推送最新 blog、以及短篇内容。


本文是 Advanced RxJava http://akarnokd.blogspot.com/ 系列博客的中文翻译,已征得作者授权。该系列博客的作者是 RxJava 的核心贡献者之一。翻译的内容使用 知识共享 署名-非商业性使用-相同方式共享 4.0 国际 协议进行许可,转载请注明出处。如果发现翻译问题,或者任何改进意见,请 在 Github 上提交 issue 。 本文是 Piasy 独立翻译,发表于 blog.piasy.com/AdvancedRxJ…,请阅读原文支持原创 blog.piasy.com/AdvancedRxJ…

原文 The Reactive-Streams API (part 4 - final)

介绍

在这篇介绍 Reactive-Streams API 的最后一篇文章中,我会讲讲我们对 SubscriptionArbiterProducerArbiter 的一个兄弟) 的高频使用需求,这一点可能会让很多人感到惊讶,当涉及到多个数据源、调度、其他异步内容时,我们将会很频繁的用到它。

在 RxJava 1.x 中,给 Subscriber 设置 Producer 是可选的,我们可以在没有 Producer 的情况下直接调用 onError()onCompleted()。这些调用最终都会调用 rx.Subscriber.unsubscribe(),并清理相关资源。

与之相反,RS 要求 Subscription 必须在 onXXX 被调用之前,通过 onSubscribe() 传递给 Subscriber

在本文中,我将展示一些这一要求可能导致的困境,尤其是当操作符的实现方式是典型的 RxJava 结构时。

稍后订阅

defer() 是必须考虑“订阅之前的错误”(error-before-Subscription)情况的操作符之一。当订阅一个推迟的 Publisher 时,操作符会先利用用户提供的工厂方法创建一个新的 Publisher,这个新的才是之后被订阅的。由于我们必须对用户的代码进行检查,所以我们就要捕获可能的异常,并且把异常通知给下游。但是,在这种情况下(要调用 child.onError()),我们就需要 child Subscriber 已经有 Subscription 了,但如果 child 这时已经有了 Subscription,那新生成的 Producer 收到 Subscription 时就不能转交给 child 了。解决方案就是使用一个 SubscriptionArbiter,它让我们可以提前发送错误事件,或者稍后切换到“真正的”数据源。

译者注:这里 SubscriptionArbiter 相当于一个占位符或者说管道,Subscriber 不能替换 Subscription,但 SubscriptionArbiter 可以,这种移花接木的思路很赞,像国内很多插件化/热修复的方案,使用自定义的 ClassLoader,想法上差不多

public final class OnSubscribeDefer
implements OnSubscribe {
    final Func0 extends Publisher extends T>> factory;
    public OnSubscribeDefer(
           Func0 extends Publisher extends T>> factory) {
        this.factory = factory;
    }
    @Override
    public void call(Subscriber super T> child) {
         
        SubscriptionArbiter sa = new SubscriptionArbiter();
        child.onSubscribe(sa);                                 // (1)  
         
        Publisher extends T> p;
        try {
            p = factory.call();
        } catch (Throwable e) {
            Exceptions.throwIfFatal(e);
            child.onError(e);                                  // (2)
            return;
        }
        p.subscribe(new Subscriber() {                      // (3)
            @Override
            public void onSubscribe(Subscription s) {
                sa.setSubscription(s);                         // (4)
            }
 
            @Override
            public void onNext(T t) {
                child.onNext(t);
            }
 
            @Override
            public void onError(Throwable t) {
                child.onError(t);
            }
 
            @Override
            public void onComplete() {
                child.onComplete();
            }
             
        });
    }
}

这一次我们不需要管理资源,但我们需要处理 Subscription 切换:

  1. 首先我们创建一个空的 arbiter,把它设置给 child。
  2. 如果用户的工厂方法抛出了异常,我们就可以安全地给 child 发送 onError 了,因为它已经有了 Subscription(尽管是个 arbiter)。
  3. 我们不能直接订阅到 child(因为它已经有了 Subscription),所以我们创建一个新的 Subscriber,并重写 onSubscribe 方法。
  4. 我们把“真正的” Subscription 设置给 arbiter,其他的事件直接转发给 child 即可。

延迟一段时间后订阅

让我们看看现在的 delaySubscription() 操作符的实现,它把实际订阅操作延迟了一定的时间。我们几乎可以把已有的实现代码拷贝过来,但由于 API 发生了变化,会发生编译问题:

public final class OnSubscribeDelayTimed
implements OnSubscribe {
    final Publisher source;
    final Scheduler scheduler;
    final long delay;
    final TimeUnit unit;
    public OnSubscribeDelayTimed(
            Publisher source, 
            long delay, TimeUnit unit, 
            Scheduler scheduler) {
        this.source = source;
        this.delay = delay;
        this.unit = unit;
        this.scheduler = scheduler;
    }
    @Override
    public void call(Subscriber child) {
        Scheduler.Worker w = scheduler.createWorker();
         
        // child.add(w);
         
        w.schedule(() -> {
            // if (!child.isUnsubscribed()) {
 
                source.subscribe(child);
 
            // }
        }, delay, unit);
    }
}

我们不能直接把资源添加到 child 中,也不能检查它是否已经被取消订阅(RS 中没有这两个方法了)。为了能清除 worker,我们就需要一个 disposable 容器,为了能够取消订阅,我们还需要一个可以“重放”数据源提供的 Subscription 取消操作的东西:

@Override
public void call(Subscriber super T> child) {
    Scheduler.Worker w = scheduler.createWorker();
     
    SubscriptionArbiter sa = new SubscriptionArbiter();    // (1)
 
    DisposableSubscription dsub = 
        new DisposableSubscription(
            sa, new DisposableList());                     // (2)
     
    dsub.add(w);                                           // (3)
     
    child.onSubscribe(dsub);                               // (4)
     
    w.schedule(() -> {
        source.subscribe(new Subscriber() {             // (5)
            @Override
            public void onSubscribe(Subscription s) {
                sa.setSubscription(s);                     // (6)
            }
 
            @Override
            public void onNext(T t) {
                child.onNext(t);
            }
 
            @Override
            public void onError(Throwable t) {
                child.onError(t);
            }
 
            @Override
            public void onComplete() {
                child.onComplete();
            }
             
        });
    }, delay, unit);

比原来的实现多了很多代码,但值得这样做:

  1. 我们需要一个 SubscriptionArbiter 来进行占位,因为实际的 Subscription 会延迟到来,所以我们需要用 arbiter 先记录下取消操作。
  2. 如果 child 要取消整个操作,那我们就需要取消这一次调度(直接取消这个 worker)。但由于 arbiter 没有资源管理能力,所以我们需要一个 disposable 容器。当然,我们只有一个资源,用不着一个 List,所以你可以实现一个自己的单一 disposable 容器类。
  3. 我们把 worker 加入到容器中,它就会替我们处理取消的事宜了。
  4. 当我们设置好 arbiter 和 disposable 之后,我们就可以把它们交给 child 了。然后 child 就可以随意进行 request() 以及 cancel() 操作了,arbiter 和 disposable 会在合适的时机(实际 Subscription 到来时),把记录下来的操作都转发给数据源/资源了。
  5. 由于我们已经给 child 设置过了 Subscription,所以我们不能在调度时直接使用 child。我们创建一个 wrapper,在收到 Subscription 时设置给 arbiter,并且转发其他的 onXXX 事件。
  6. 我们在把实际的 Subscription 设置给 arbiter 时,arbiter 会重放积累的 request/cancel 操作。

现在你可能觉得最终调度时的 Subscriber(5)有错误,它没有在 onNext 中调用 sa.produced(1)。这确实会导致请求量计数不一致,但是一旦实际的 Subscription 被设置之后,后续 child 的 request(n) 都会原封不动地转发给上游,而我们后面又不会再调用 setSubscription() 了。所以上游能收到正确的请求量,即便 arbiter 计数不一致,也不会导致任何问题。为了保证更安全,你可以:

  • onNext 中调用 sa.produced(1)
  • 或者实现一个自己的 arbiter,只接受一个 Subscription,并且在收到它之后停止计数。

总结

在本文中,我展示了两种使用 SubscriptionArbiter 的场景。幸运的是,不是所有的操作符都需要进行这样的操作,但主流的都需要,因此在处理“实际” Subscription 延迟抵达以及 cancel() 的同时释放资源的问题时,我们需要两种特殊的 SubscriptionSubscriptionArbiterDisposableSubscription

由于这种情况会频繁出现,我相信 RxJava 2.0 会提供一种标准而且高效的实现方案,来帮助我们遵循 Reactive-Streams 规范。

作为 RS API 系列的总结,RS 最小的接口集合就能满足典型异步数据流场景的需求了。我们可以把 RxJava 的思想迁移到 RS 中,但基础设施以及一些辅助工具都需要从头开始实现。我已经展示了几种基本的 SubscriptionSingeSubscriptionSingleDelayedSubscriptionSubscriptionArbiter 以及 DisposableSubscription。再加上一些其他类似的工具类,它们将是实现 RxJava 2.0 操作符的主要工具类。

在下一个系列中,我将讲解响应式编程中争议最多的一个类型(Subject),而且我不仅会讲到它们的细节,还会讲如何实现我们自己的变体。

欢迎大家关注我的微信公众号,会推送最新 blog、以及短篇内容。