【问题标题】:"Merging" a stream of streams to produce a stream of the latest values of each“合并”流的流以产生每个流的最新值的流
【发布时间】:2013-07-28 17:28:56
【问题描述】:

我有一个IObservable<IObservable<T>>,其中每个内部IObservable<T> 是一个值流,后跟一个最终的OnCompleted 事件。

我想将其转换为IObservable<IEnumerable<T>>,这是一个由任何未完成的内部流中的最新值组成的流。每当从内部流之一生成新值(或内部流过期)时,它应该生成一个新的IEnumerable<T>

最容易用大理石图表示(我希望它足够全面):

input ---.----.---.----------------
         |    |   '-f-----g-|      
         |    'd------e---------|
         'a--b----c-----|          

result ---a--b-b--c-c-c-e-e-e---[]-
               d  d d e f g        
                    f f            

[] 是空的IEnumerable<T>-| 代表 OnCompleted)

您可以看到它有点类似于CombineLatest 操作。 我一直在玩 JoinGroupJoin 无济于事,但我觉得这几乎肯定是前进的正确方向。

我想在这个运算符中使用尽可能少的状态。

更新

我已更新此问题,不仅包括单值序列 - 结果 IObservable<IEnumerable<T>> 应仅包括每个序列的最新值 - 如果序列未产生值,则不应包括在内。

【问题讨论】:

  • 显然你需要一些状态。
  • @LasseV.Karlsen 你能解释一下原因吗?
  • 没有状态是不可能的,因为根据定义,操作员需要跟踪每个内部序列,直到它完成。
  • 您昨天的解决方案只需稍作调整即可工作。如果你能重新发布它,我会告诉你调整。
  • @Brandon - 我已取消删除我相信您的意思的解决方案。感谢您的帮助,非常感谢

标签: c# system.reactive


【解决方案1】:

这是基于您昨天解决方案的版本,针对新要求进行了调整。基本思想是将引用放入易腐烂的集合中,然后在内部序列产生新值时更新引用的值。

我还进行了修改以正确跟踪内部订阅并在外部 observable 取消订阅时取消订阅。

如果任何流产生错误,也进行了修改以将其全部删除。

最后,我修复了一些可能违反 Rx 指南的竞争条件。如果你的内部 observables 从不同的线程同时触发,你可以同时调用obs.OnNext,这是一个很大的禁忌。因此,我使用相同的锁对每个内部 observable 进行了门控以防止这种情况发生(请参阅Synchronize 调用)。请注意,正因为如此,您可能会使用常规的双链表而不是 PerishableCollection,因为现在使用该集合的所有代码都在锁内,因此您不需要 @987654324 的线程保证@。

// Acts as a reference to the current value stored in the list
private class BoxedValue<T>
{
    public T Value;
    public BoxedValue(T initialValue) { Value = initialValue; }
}

public static IObservable<IEnumerable<T>> MergeLatest<T>(this IObservable<IObservable<T>> source)
{
    return Observable.Create<IEnumerable<T>>(obs =>
    {
        var collection = new PerishableCollection<BoxedValue<T>>();
        var outerSubscription = new SingleAssignmentDisposable();
        var subscriptions = new CompositeDisposable(outerSubscription);
        var innerLock = new object();

        outerSubscription.Disposable = source.Subscribe(duration =>
        {
            BoxedValue<T> value = null;
            var lifetime = new DisposableLifetime(); // essentially a CancellationToken
            var subscription = new SingleAssignmentDisposable();

            subscriptions.Add(subscription);
            subscription.Disposable = duration.Synchronize(innerLock)
                .Subscribe(
                    x =>
                    {
                        if (value == null)
                        {
                            value = new BoxedValue<T>(x);
                            collection.Add(value, lifetime.Lifetime);
                        }
                        else
                        {
                            value.Value = x;
                        }
                        obs.OnNext(collection.CurrentItems().Select(p => p.Value.Value));
                    },
                    obs.OnError, // handle an error in the stream.
                    () => // on complete
                    {
                        if (value != null)
                        {
                            lifetime.Dispose(); // removes the item
                            obs.OnNext(collection.CurrentItems().Select(p => p.Value.Value));
                            subscriptions.Remove(subscription); // remove this subscription
                        }
                    }
            );
        });

        return subscriptions;
    });
}

【讨论】:

  • 谢谢你 - 它很容易理解,尽管我确实想知道是否可以以不可变的方式进行。如果可以的话,还有另一个 +1 以增加线程安全!
  • 次要点:.NET Framework 提供的System.Runtime.CompilerServices.StrongBox 实际上与您的BoxedValue 相同。
【解决方案2】:

此解决方案适用于单项流,但不幸的是会将每个项累积到内部流中,直到完成。

public static IObservable<IEnumerable<T>> MergeLatest<T>(this IObservable<IObservable<T>> source)
{
    return Observable.Create<IEnumerable<T>>(obs =>
    {
        var collection = new PerishableCollection<T>();
        return source.Subscribe(duration =>
        {
            var lifetime = new DisposableLifetime(); // essentially a CancellationToken
            duration
                .Subscribe(
                    x => // on initial item
                    {
                        collection.Add(x, lifetime.Lifetime);
                        obs.OnNext(collection.CurrentItems().Select(p => p.Value));
                    },
                    () => // on complete
                    {
                        lifetime.Dispose(); // removes the item
                        obs.OnNext(collection.CurrentItems().Select(p => p.Value));
                    }
            );
        });
    });
}

【讨论】:

    【解决方案3】:

    Another solution given by Dave SextonRxx 的创建者 - 它使用 Rxx.CombineLatest,在实现上似乎与 Brandon 的解决方案非常相似:

    public static IObservable<IEnumerable<T>> CombineLatestEagerly<T>(this IObservable<IObservable<T>> source)
    {
      return source
        // Reify completion to force an additional combination:
        .Select(o => o.Select(v => new { Value = v, HasValue = true })
                      .Concat(Observable.Return(new { Value = default(T), HasValue = false })))
        // Merge a completed observable to force combination with the first real inner observable:
        .Merge(Observable.Return(Observable.Return(new { Value = default(T), HasValue = false })))
        .CombineLatest()
        // Filter out completion notifications:
        .Select(l => l.Where(v => v.HasValue).Select(v => v.Value));
    }
    

    【讨论】:

    • 是的,我的意思是说 Rxx 有一个 CombineLatest 重载,几乎可以满足您的要求。对于那个很抱歉。由于我在我的项目中使用 Rxx,如果我必须解决这个问题,这就是我会使用的解决方案。代码比我的小,比马修的更容易理解。
    • 我有点担心一旦内部流完成,最后一个值会留在集合中(即使它没有被看到)。这意味着随着时间的推移,如果有足够的流,“剩余”项目的数量会变得非常大。使用您的解决方案,一旦流完成,盒装值就会从集合中删除。
    • 啊是的,这是我没有意识到的问题。
    猜你喜欢
    • 2015-04-13
    • 2019-12-06
    • 2018-08-07
    • 1970-01-01
    • 1970-01-01
    • 2019-10-05
    • 2017-06-06
    • 2019-07-10
    • 1970-01-01
    相关资源
    最近更新 更多