RxJava:为什么为每个observables分支重新计算相同的转换?

时间:2014-11-26 10:49:46

标签: system.reactive rx-java

简介

考虑简单的java代码。它根据a定义了两个可观察量bcd本身是使用Observable<Integer> 定义的(a,b,c,d具有类型{{1} })

    d = Observable.range(1, 10);
    c = d.map(t -> t + 1);
    a = c.map(t -> t + 2);
    b = c.map(t -> t + 3);

此代码可以使用图表显示,其中每个箭头(->)代表转换(地图方法)

             .--> a
   d --> c --|
             '--> b

如果几个可观察链具有自己的部分,那么(理论上)公共部分的新值只能计算一次。在上面的示例中:每个新的d值只能转换为d --> c一次,并同时用于ab

问题

在实践中,我观察到使用test的每个链计算转换。换句话说,上面的例子应该像这样正确绘制:

   d --> c --> a
   d --> c --> b

在资源消耗转换的情况下,链末尾的新订阅将导致整个链的计算(和性能损失)。

强制转换结果的正确方法是强制转换和计算吗?

我的研究

我找到了两个解决此问题的方法:

  1. 将唯一标识符与值一起传递并将转换结果存储在某些外部存储(rx库外部)
  2. 使用主题实现隐藏可观察链的开头的类似地图的功能。 MapOnce code; test
  3. 两者都有效。第二个很简单,但闻起来像黑客。

2 个答案:

答案 0 :(得分:3)

您已确定hot and cold observables

Observable.range会返回可观察对象,但您可以在层次结构中描述生成的查询,就好像他们;即,好像他们分享订阅副作用。他们不。每次订阅可观察时,都可能导致副作用。在您的情况下,每次订阅range(或在range上建立的查询)时,它都会生成一系列值。

在研究的第二点,您已经确定了如何将 observable转换为 hot observable;即使用主题。 (虽然在.NET中你不能直接使用Subject<T>;相反,你使用像Publish这样的运算符。我怀疑RxJava有一个类似的运算符而且我是建议使用它。)

其他详细信息

我的解释中 hot 的定义,如我上面链接的博客文章中详细描述的那样,当一个可观察不会导致任何订阅副作用时。 (请注意,从 cold 转换为 hot 时, hot observable可能会多播连接副作用,但温度仅指参考因为在实践中谈论可观察的温度时我们真正关心的是所有我们真正关心的可观察性倾向导致订阅副作用。)

我在博客文章的结论中提到的map运算符(.NET中的Select)返回一个继承其源的温度的可观察对象,所以在你的底部图cab ,因为d 。假设您将publish应用于d,那么cab会继承温度来自已发布的observable,意味着订阅它们不会导致任何订阅副作用。因此,发布d会将可观察对象range转换为 hot 可观察对象。

    .--> c --> a
d --|
    .--> c --> b

但是,您的问题是如何分享c以及d的计算方式。即使您要发布dc仍会针对来自a的每个通知重新计算bd。相反,您希望在ca之间分享b的结果。我打电话给你想要分享其计算副作用的观察者,&#34; 活跃&#34;。 (我借用神经科学中使用的被动&amp; 活动术语来描述神经元中的电化学电流。)

在您的顶部图表中,您正在考虑c 活动,因为它会导致显着的计算副作用,这是您自己的解释。请注意,c 有效,无论d的温度如何。要分享 active observable的计算副作用,或许令人惊讶,您必须使用publish,就像 cold observable一样。这是因为技术上活动计算的副作用与可观察量相同,而被动计算没有副作用,就像观察。我已将 hot cold 这些术语限制为仅引用初始计算副作用,我称之为订阅方效果,因为人们通常如何使用它们。我已经引入了新术语 active passive ,与计划副作用分开引用计算副作用。

结果是这些术语在实践中直观地融合在一起。如果您想分享c的计算副作用,那么只需publish而不是d。通过这样做,ab隐式变为 hot ,因为map继承了订阅副作用,如前所述。因此,您可以通过发布dc来有效地制作可观察的热门的右侧,但发布c也会共享其计算副作用

如果您发布c而不是d,那么d仍然,但自c隐藏以来无关紧要来自da的{​​{1}}。因此,通过发布b,您也可以有效地发布c。因此,在您的observable中的任何位置应用d会使observable的右侧有效 hot 。在您引入publish的位置或您在可观察对象右侧创建的观察者或管道的数量并不重要。但是,选择发布publish而不是c也会分享d的计算副作用,这在技术上会完成您问题的答案。 Q.E.D。

答案 1 :(得分:1)

Observable每次订阅时都会被懒散地执行(通过合成显式或隐式)。

此代码显示源如何为a,b和c发出:

    Observable<Integer> d = Observable.range(1, 10)
            .doOnNext(i -> System.out.println("Emitted from source: " + i));
    Observable<Integer> c = d.map(t -> t + 1);
    Observable<Integer> a = c.map(t -> t + 2);
    Observable<Integer> b = c.map(t -> t + 3);

    a.forEach(i -> System.out.println("a: " + i));
    b.forEach(i -> System.out.println("b: " + i));
    c.forEach(i -> System.out.println("c: " + i));

如果你可以缓慢(缓存)结果,那么就像使用.cache()运算符一样简单。

    Observable<Integer> d = Observable.range(1, 10)
            .doOnNext(i -> System.out.println("Emitted from source: " + i))
            .cache();

    Observable<Integer> c = d.map(t -> t + 1);
    Observable<Integer> a = c.map(t -> t + 2);
    Observable<Integer> b = c.map(t -> t + 3);

    a.forEach(i -> System.out.println("a: " + i));
    b.forEach(i -> System.out.println("b: " + i));
    c.forEach(i -> System.out.println("c: " + i));

将.cache()添加到源中会使它只发出一次并且可以多次订阅。

对于大型或无限数据源,缓存不是一种选择,因此多播是确保源仅发出一次的解决方案。

publish()和share()运算符是一个很好的起点,但为简单起见,因为这是一个同步的例子,我将使用通常最容易使用的publish(function)重载来显示。

    Observable<Integer> d = Observable.range(1, 10)
            .doOnNext(i -> System.out.println("Emitted from source: " + i))
            .publish(oi -> {
                Observable<Integer> c = oi.map(t -> t + 1);
                Observable<Integer> a = c.map(t -> t + 2);
                Observable<Integer> b = c.map(t -> t + 3);

                return Observable.merge(a, b, c);
            });

    d.forEach(System.out::println);

如果a,b,c单独使用,那么我们可以将所有内容连接起来,并在准备就绪时“连接”来源:

private static void publishWithConnect() {
    ConnectableObservable<Integer> d = Observable.range(1, 10)
            .doOnNext(i -> System.out.println("Emitted from source: " + i))
            .publish();

    Observable<Integer> c = d.map(t -> t + 1);
    Observable<Integer> a = c.map(t -> t + 2);
    Observable<Integer> b = c.map(t -> t + 3);

    a.forEach(i -> System.out.println("a: " + i));
    b.forEach(i -> System.out.println("b: " + i));
    c.forEach(i -> System.out.println("c: " + i));

    // now that we've wired up everything we can connect the source
    d.connect();
}

如果源是异步的,我们可以使用refCounting:

    Observable<Integer> d = Observable.range(1, 10)
            .doOnNext(i -> System.out.println("Emitted from source: " + i))
            .subscribeOn(Schedulers.computation())
            .share();

但是,refCount(共享是提供它的重载)允许竞争条件,因此不能保证所有订阅者都获得第一个值。通常只需要订阅者来来往往的“热门”流。对于我们希望确保每个人都能获得的“冷”源,以前使用cache()或publish()/ publish(function)的解决方案是首选方法。

您可以在此处了解详情:https://github.com/ReactiveX/RxJava/wiki/Connectable-Observable-Operators