简体   繁体   English

如何与发布和连接共享一个observable?

[英]How do I share an observable with publish and connect?

I have an observable data stream that I am applying operations to, splitting into two separate streams, applying more (distinct) operations to each of the two streams, and merging together again. 我有一个可观察的数据流,我正在应用操作,分成两个独立的流,对两个流中的每一个流应用更多(不同的)操作,并再次合并在一起。 I am trying to share the observable between two subscribers using Publish and Connect but each of the subscribers seems to be using a separate stream. 我试图使用Publish and Connect分享两个订阅者之间的可观察性,但每个订阅者似乎都在使用单独的流。 That is, in the example below, I see "Doing an expensive operation" printed once for each item in the stream for both of the subscribers . 也就是说,在下面的示例中,我看到两个订阅者为流中的每个项目打印一次“执行昂贵的操作”。 (Imagine the expensive operation as being something that should happen only once between all subscribers, as such I am trying to reuse the stream.) I have used Publish and Connect to try and share the merged observable with both subscribers, but it seems to have the wrong effect. (想象一下,昂贵的操作是在所有订阅者之间只发生一次的事情,因此我试图重用流。)我使用Publish and Connect尝试与两个订阅者共享合并的observable,但它似乎有错误的效果。

Example with the issue: 问题示例:

var foregroundScheduler = new NewThreadScheduler(ts => new Thread(ts) { IsBackground = false });
var timer = Observable.Timer(TimeSpan.Zero, TimeSpan.FromSeconds(10), foregroundScheduler);
var expensive = timer.Select(i =>
{
    // Converting to strings is an expensive operation
    Console.WriteLine("Doing an expensive operation");
    return string.Format("#{0}", i);
});

var a = expensive.Where(s => int.Parse(s.Substring(1)) % 2 == 0).Select(s => new { Source = "A", Value = s });
var b = expensive.Where(s => int.Parse(s.Substring(1)) % 2 != 0).Select(s => new { Source = "B", Value = s });

var connectable = Observable.Merge(a, b).Publish();
connectable.Where(x => x.Source.Equals("A")).Subscribe(s => Console.WriteLine("Subscriber A got: {0}", s));
connectable.Where(x => x.Source.Equals("B")).Subscribe(s => Console.WriteLine("Subscriber B got: {0}", s));
connectable.Connect();

I see the following output: 我看到以下输出:

Doing expensive operation
Doing expensive operation
Subscriber A got: { Source = A, Value = #0 }
Doing expensive operation
Doing expensive operation
Subscriber B got: { Source = B, Value = #1 }

(Output continues, truncated for brevity.) (输出继续,为简洁而截断。)

How can I share the observable with both subscribers? 如何与两个订阅者共享observable?

You have published the wrong observable. 您发布了错误的可观察对象。

With the current code you are merging and then publishing like this Observable.Merge(a, b).Publish(); 使用当前代码,您正在合并然后发布像Observable.Merge(a, b).Publish(); . Now since a & b are defined against expensive you still get two subscriptions to expensive . 现在因为ab的定义是expensive你仍然可以获得两个expensive订阅。

The subscriptions create these pipelines: 订阅会创建这些管道:

原版的

You can see this if you take out the .Publish(); 如果你拿出.Publish();你可以看到这个.Publish(); from your code. 从你的代码。 The output becomes: 输出变为:

Doing an expensive operation
Doing an expensive operation
Doing an expensive operation
Doing an expensive operation
Subscriber A got: { Source = A, Value = #0 }
Doing an expensive operation
Doing an expensive operation
Doing an expensive operation
Doing an expensive operation
Subscriber B got: { Source = B, Value = #1 }

This creates these pipelines: 这会创建这些管道:

没有发布

So, by shifting the .Publish() back up to expensive you eliminate the problem. 因此,通过将.Publish()更改为expensive您可以消除问题。 That's where you really needed it because it is the expensive operation after all. 这就是你真正需要它的地方,因为它毕竟是昂贵的操作。

This is the code you needed: 这是您需要的代码:

var foregroundScheduler = new NewThreadScheduler(ts => new Thread(ts) { IsBackground = false });
var timer = Observable.Timer(TimeSpan.Zero, TimeSpan.FromSeconds(10), foregroundScheduler);
var expensive = timer.Select(i =>
{
    // Converting to strings is an expensive operation
    Console.WriteLine("Doing an expensive operation");
    return string.Format("#{0}", i);
});

var connectable = expensive.Publish();

var a = connectable.Where(s => int.Parse(s.Substring(1)) % 2 == 0).Select(s => new { Source = "A", Value = s });
var b = connectable.Where(s => int.Parse(s.Substring(1)) % 2 != 0).Select(s => new { Source = "B", Value = s });

var merged = Observable.Merge(a, b);

merged.Where(x => x.Source.Equals("A")).Subscribe(s => Console.WriteLine("Subscriber A got: {0}", s));
merged.Where(x => x.Source.Equals("B")).Subscribe(s => Console.WriteLine("Subscriber B got: {0}", s));

connectable.Connect();

That nicely produces the following: 这很好地产生了以下内容:

Doing an expensive operation
Subscriber A got: { Source = A, Value = #0 }
Doing an expensive operation
Subscriber B got: { Source = B, Value = #1 }
Doing an expensive operation
Subscriber A got: { Source = A, Value = #2 }
Doing an expensive operation
Subscriber B got: { Source = B, Value = #3 }

And this gives you these pipelines: 这给你这些管道:

昂贵的发布

You can see from this image that there is still duplication. 您可以从此图像中看到仍然存在重复。 That's fine because these parts aren't expensive. 这很好,因为这些部件并不昂贵。

The duplication is actually important. 复制实际上很重要。 Shared parts of the pipelines make their endpoints vulnerable to errors and thus to early termination. 管道的共享部分使其端点易受错误影响,从而提前终止。 The less sharing the better for the robustness of the code. 共享越少,代码的健壮性就越好。 It's only when you have an expensive operation that you should worry about publishing. 只有当您进行昂贵的操作时才应该担心发布。 Otherwise you should just let the pipelines be themselves. 否则你应该让管道成为自己。

Here's an example to show it. 这是一个展示它的例子。 If you don't have a published source then, if one source produces an error then it doesn't pull down all of the pipelines. 如果您没有已发布的源,那么,如果一个源产生错误,那么它不会下拉所有管道。

分离

But once you introduce a shared observable then a single error will bring down all of the pipelines. 但是一旦你引入了一个共享的observable,那么一个错误就会降低所有的管道。

共享

One possible fix: 一个可能的解决方

var foregroundScheduler = new NewThreadScheduler(ts => new Thread(ts) { IsBackground = false });
var timer = Observable.Timer(TimeSpan.Zero, TimeSpan.FromSeconds(10), foregroundScheduler);
var expensive = timer.Select(i =>
{
    // Converting to strings is an expensive operation
    Console.WriteLine("Doing an expensive operation");
    return string.Format("#{0}", i);
});

var subj = new ReplaySubject<string>();
expensive.Subscribe(subj);

var a = subj.Where(s => int.Parse(s.Substring(1)) % 2 == 0).Select(s => new { Source = "A", Value = s });
var b = subj.Where(s => int.Parse(s.Substring(1)) % 2 != 0).Select(s => new { Source = "B", Value = s });

var merged = Observable.Merge(a, b);
merged.Where(x => x.Source.Equals("A")).Subscribe(s => Console.WriteLine("Subscriber A got: {0}", s));
merged.Where(x => x.Source.Equals("B")).Subscribe(s => Console.WriteLine("Subscriber B got: {0}", s));

The above example essentially creates a new intermediate observable that emits the results of the expensive operation. 上面的示例实际上创建了一个新的中间可观察对象,它发出了昂贵操作的结果。 This allows you to subscribe to the results of the expensive operation, not to an expensive transformation applied to a timer. 这允许您订阅昂贵操作的结果,而不是应用于计时器的昂贵转换。

With this you'll see: 有了这个,你会看到:

Doing an expensive operation
Subscriber A got: { Source = A, Value = #0 }
Doing an expensive operation
Subscriber B got: { Source = B, Value = #1 }

(Output continues, truncated for brevity.) (输出继续,为简洁而截断。)

Alternatively, you could move the calls to Publish and Connect : 或者,您可以将调用移至PublishConnect

var foregroundScheduler = new NewThreadScheduler(ts => new Thread(ts) {IsBackground = false});
var timer = Observable.Timer(TimeSpan.Zero, TimeSpan.FromSeconds(10), foregroundScheduler);
var expensive = timer.Select(i =>
{
    // Converting to strings is an expensive operation
    Console.WriteLine("Doing an expensive operation");
    return string.Format("#{0}", i);
}).Publish();

var a = expensive.Where(s => int.Parse(s.Substring(1)) % 2 == 0).Select(s => new { Source = "A", Value = s });
var b = expensive.Where(s => int.Parse(s.Substring(1)) % 2 != 0).Select(s => new { Source = "B", Value = s });

var merged = Observable.Merge(a, b);
merged.Where(x => x.Source.Equals("A")).Subscribe(s => Console.WriteLine("Subscriber A got: {0}", s));
merged.Where(x => x.Source.Equals("B")).Subscribe(s => Console.WriteLine("Subscriber B got: {0}", s));

expensive.Connect();

Why ReplaySubject , not just Subject or some other subject? 为什么选择ReplaySubject ,而不仅仅是Subject或其他主题?

A Subject , in the .NET Rx implementation is by default what the ReactiveX documentation calls a PublishSubject , which emits to an observer only those items that are emitted by the source Observable subsequent to the time of the subscription. 默认情况下,.NET Rx实现中的SubjectReactiveX文档调用PublishSubject ,该发布PublishSubject仅向观察者发出在订阅时间之后由源Observable发出的项目。 A ReplaySubject on the other hand, emits to any observer all of the items that were emitted by the source Observable, regardless of when the observer subscribes . 另一方面, ReplaySubject向任何观察者发出源Observable发出的所有项目, 无论观察者何时订阅 If we use a plain subject in the first example, the subscription of subj to the timer will cause subscriptions to subj to miss anything emitted between the time that the subject subscribes to the expensive operation and the time that they subscribe to the intermediate subject ( subj ). 如果我们在第一个示例中使用普通主题,则将subj订阅到计时器将导致对subj订阅错过在主题订阅昂贵操作的时间与他们订阅中间主题的时间之间发出的任何内容( subj )。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM