C# 如何修复Publish().RefCount()行为的不一致性?
最近,我偶然发现了一个关于和运算符的神秘性: 您使用的是危险的.Publish().RefCount()运算符对,它创建了一个在完成后无法订阅的序列 这一声明似乎反对李·坎贝尔对这些运营商的评估。引用他的书: Publish/RefCount对用于获取冷可观测数据并将其作为热可观测序列共享给后续观察者非常有用 起初,我不相信“谜”的说法是正确的,所以我试图反驳它。我的实验表明,C# 如何修复Publish().RefCount()行为的不一致性?,c#,system.reactive,rx.net,C#,System.reactive,Rx.net,最近,我偶然发现了一个关于和运算符的神秘性: 您使用的是危险的.Publish().RefCount()运算符对,它创建了一个在完成后无法订阅的序列 这一声明似乎反对李·坎贝尔对这些运营商的评估。引用他的书: Publish/RefCount对用于获取冷可观测数据并将其作为热可观测序列共享给后续观察者非常有用 起初,我不相信“谜”的说法是正确的,所以我试图反驳它。我的实验表明,Publish().RefCount()可以 确实不一致。第二次订阅已发布序列可能会导致对源序列的新订阅,这取决于源序列
Publish().RefCount()
可以
确实不一致。第二次订阅已发布序列可能会导致对源序列的新订阅,这取决于源序列在连接时是否已完成。如果已完成,则不会重新订阅。如果未完成,则将重新订阅。以下是此行为的演示:
var observable = Observable
.Create<int>(o =>
{
o.OnNext(13);
o.OnCompleted(); // Commenting this line alters the observed behavior
return Disposable.Empty;
})
.Do(x => Console.WriteLine($"Producer generated: {x}"))
.Finally(() => Console.WriteLine($"Producer finished"))
.Publish()
.RefCount()
.Do(x => Console.WriteLine($"Consumer received #{x}"))
.Finally(() => Console.WriteLine($"Consumer finished"));
observable.Subscribe().Dispose();
observable.Subscribe().Dispose();
()
如果我们对o.OnCompleted()进行注释,则输出如下:代码>行。这种微妙的变化会导致一种预期和可取的行为:
Producer生成:13
消费者收到#13
制作人完成
消费者完成
生产者:13
消费者收到#13
制作人完成
消费者完成
在第一种情况下,cold producer(发布().RefCount()
之前的零件)只订阅了一次。第一个消费者收到了发出的值,但第二个消费者没有收到任何东西(除了OnCompleted
通知)。在第二种情况下,制作人被订阅了两次。每次它产生一个值,每个消费者得到一个值
我的问题是:我们如何解决这个问题?我们如何修改Publish
操作符或RefCount
,或两者都修改,以使它们始终保持一致且符合要求的行为?以下是理想行为的规范:
var observable = Observable
.Create<int>(o =>
{
o.OnNext(13);
o.OnCompleted(); // Commenting this line alters the observed behavior
return Disposable.Empty;
})
.Do(x => Console.WriteLine($"Producer generated: {x}"))
.Finally(() => Console.WriteLine($"Producer finished"))
.Publish()
.RefCount()
.Do(x => Console.WriteLine($"Consumer received #{x}"))
.Finally(() => Console.WriteLine($"Consumer finished"));
observable.Subscribe().Dispose();
observable.Subscribe().Dispose();
PublishRefCount
操作符,或者使用内置操作符实现所需功能的方法
顺便说一句,存在一个问题,那就是为什么会发生这种情况。我的问题是如何修复它
更新:回顾过去,上述规范导致了一种不稳定的行为,使得竞争条件不可避免。不能保证对已发布序列的两个订阅将导致对源序列的单个订阅。源序列可能在两个订阅之间完成,导致第一个订阅方取消订阅,导致
RefCount
操作员取消订阅,导致下一个订阅方对源进行新订阅。内置的.Publish().RefCount()
的行为防止了这种情况的发生
道德教训是,.Publish().RefCount()
序列没有中断,但它是不可重用的。它不能可靠地用于多个连接/断开连接会话。如果您想要第二个会话,您应该创建一个新的.Publish().RefCount()
序列。Lee做了一个解释IConnectableObservable
,但是Publish
没有解释得那么清楚。这是一种很简单的动物,很难解释。我假设您理解I可连接可观察的:
如果我们想简单而懒惰地重新实现zero-paramPublish
函数,它会是这样的:
// For illustrative purposes only: don't use this code
public class PublishObservable<T> : IConnectableObservable<T>
{
private readonly IObservable<T> _source;
private readonly Subject<T> _proxy = new Subject<T>();
private IDisposable _connection;
public PublishObservable(IObservable<T> source)
{
_source = source;
}
public IDisposable Connect()
{
if(_connection == null)
_connection = _source.Subscribe(_proxy);
var disposable = Disposable.Create(() =>
{
_connection.Dispose();
_connection = null;
});
return _connection;
}
public IDisposable Subscribe(IObserver<T> observer)
{
var _subscription = _proxy.Subscribe(observer);
return _subscription;
}
}
public static class X
{
public static IConnectableObservable<T> Publish<T>(this IObservable<T> source)
{
return new PublishObservable<T>(source);
}
}
代码多一点,但仍然很简单:如果refcount上升到1,则在ConnectableObservable
上调用Connect
,如果refcount下降到0,则断开连接
将两者放在一起,就得到了一对,保证只有一个并发订阅可以通过一个持久的主题
进行代理。主题
将仅订阅源,而下游订阅>0
鉴于这一介绍,您的问题中存在许多误解,因此我将逐一进行讨论:
。。。Publish().RefCount()确实可能不一致。订阅第二个
发布序列的时间可能会导致对
是否为源序列,取决于源序列是否为
连接时已完成。如果它已经完成了,那么它就不会
重新订阅。如果未完成,则将重新订阅
.Publish().RefCount()
将仅在一种情况下重新订阅源:从零订阅服务器变为1。如果由于任何原因订阅者的数量从0到1到0到1,那么您将重新订阅。源可观测完成将导致RefCount
发出OnCompleted
,其所有观察者取消订阅。因此,对RefCount
的后续订阅将触发重新订阅源的尝试。当然,如果源正确地遵守可观察契约,它将立即发出一个OnCompleted
,就是这样
[参见样本observable with OnCompleted…]可观测值订阅两次。这个
预期的行为是每个订阅将收到一个
价值观
否。预期的行为是代理主题在发出oncomplete之后
var o = Observable.Interval(TimeSpan.FromMilliseconds(100))
.Take(5);
var s1 = o.Subscribe(i => Console.WriteLine(i.ToString()));
await Task.Delay(TimeSpan.FromMilliseconds(600));
var s2 = o.Subscribe(i => Console.WriteLine(i.ToString()));
public IConnectableObservable<TSource> Publish<TSource>(IObservable<TSource> source)
{
return source.Multicast(new Subject<TSource>());
}
public class StatelessSubject<T> : ISubject<T>
{
private IImmutableList<IObserver<T>> _observers
= ImmutableArray<IObserver<T>>.Empty;
public void OnNext(T value)
{
foreach (var observer in Volatile.Read(ref _observers))
observer.OnNext(value);
}
public void OnError(Exception error)
{
foreach (var observer in Volatile.Read(ref _observers))
observer.OnError(error);
}
public void OnCompleted()
{
foreach (var observer in Volatile.Read(ref _observers))
observer.OnCompleted();
}
public IDisposable Subscribe(IObserver<T> observer)
{
ImmutableInterlocked.Update(ref _observers, x => x.Add(observer));
return Disposable.Create(() =>
{
ImmutableInterlocked.Update(ref _observers, x => x.Remove(observer));
});
}
}
.Multicast(new StatelessSubject<SomeType>()).RefCount()
/// <summary>
/// Returns a connectable observable sequence that shares a single subscription to
/// the underlying sequence, without maintaining its state.
/// </summary>
public static IConnectableObservable<TSource> StatelessPublish<TSource>(
this IObservable<TSource> source)
{
return source.Multicast(new StatelessSubject<TSource>());
}
.StatelessPublish().RefCount()