转发具有冷却时间的Rx项目,当它们过快时切换到采样

时间:2013-12-18 02:02:12

标签: .net system.reactive throttling

我正在寻找一个Rx方法,该方法将采用一个可观察的并将最新项目置于“冷却时间”,这样当物品的进入速度低于冷却时间时,它们只是被转发但是当它们进入时你可以在每个冷却时间后获得最新值。

换句话说,当项目间隔时间小于t时,我希望切换到期间为t的采样(并在它们展开时切换回来)。

这与Observable.Throttle的确非常相似,只是每当新项目到达时都不会重置计时器。

我想到的应用程序是通过网络发送“最新价值”更新。我不想传达一个价值,除非它已经改变,我不想发送快速变化的价值,以至于我淹没了其他数据。

是否有标准方法可以满足我的需求?

4 个答案:

答案 0 :(得分:4)

Strilanc,考虑到你对源流安静时不需要的活动的关注,你可能会对这种节奏事件的方法感兴趣 - 我不打算另外添加这个,因为我认为J. Lennon的实现是完全合理的(而且更简单),计时器的性能不会受到伤害。

此实现还有另一个有趣的区别 - 它与Sample方法不同,因为它会立即发出在冷却时间之外而不是在下一个采样间隔发生的事件。它在冷却时间之外没有计时器。

编辑 - 这是v3解决了Chris在评论中提到的问题 - 它确保在冷却过程中发生的变化会触发新的冷却期。

    public static IObservable<T> LimitRate<T>(
        this IObservable<T> source, TimeSpan duration, IScheduler scheduler)
    {
        return source.DistinctUntilChanged()
                     .GroupByUntil(k => 0,
                                   g => Observable.Timer(duration, scheduler))
            .SelectMany(x => x.FirstAsync()
                              .Merge(x.Skip(1)
                                      .TakeLast(1)))
                              .Select(x => Observable.Return(x)
                                .Concat(Observable.Empty<T>()
                                    .Delay(duration, scheduler)))
                                    .Concat();
    }

这最初使用GroupByUntil将所有事件打包到冷却期间的同一组中。它会监视更改并在组到期时发出最终更改(如果有)。

然后将生成的事件投影到OnCompleted被冷却期延迟的流中。然后将这些流连接在一起。这可以防止事件比冷却更紧密,但是否则会尽快发出。

以下是使用nuget包rx-testingnunit运行的单元测试(针对v3编辑更新):

public class LimitRateTests : ReactiveTest
{
    [Test]
    public void SlowerThanRateIsUnchanged()
    {
        var scheduler = new TestScheduler();

        var source = scheduler.CreateColdObservable(
            OnNext(200, 1),
            OnNext(400, 2),
            OnNext(700, 3));

        var results = scheduler.CreateObserver<int>();

        source.LimitRate(TimeSpan.FromTicks(100), scheduler).Subscribe(results);

        scheduler.Start();

        results.Messages.AssertEqual(
            OnNext(200, 1),
            OnNext(400, 2),
            OnNext(700, 3));
    }

    [Test]
    public void FasterThanRateIsSampled()
    {
        var scheduler = new TestScheduler();

        var source = scheduler.CreateColdObservable(
            OnNext(100, 1),
            OnNext(140, 5),
            OnNext(150, 2),
            OnNext(300, 3),
            OnNext(350, 4));

        var results = scheduler.CreateObserver<int>();

        source.LimitRate(TimeSpan.FromTicks(100), scheduler).Subscribe(results);

        scheduler.Start();

        results.Messages.AssertEqual(
            OnNext(100, 1),
            OnNext(200, 2),
            OnNext(300, 3),
            OnNext(400, 4));
    }

    [Test]
    public void DuplicatesAreOmitted()
    {
        var scheduler = new TestScheduler();

        var source = scheduler.CreateColdObservable(
            OnNext(100, 1),
            OnNext(150, 1),
            OnNext(300, 1),
            OnNext(350, 1));

        var results = scheduler.CreateObserver<int>();

        source.LimitRate(TimeSpan.FromTicks(100), scheduler).Subscribe(results);

        scheduler.Start();

        results.Messages.AssertEqual(
            OnNext(100, 1));
    }

    [Test]
    public void CoolResetsCorrectly()
    {
        var scheduler = new TestScheduler();

        var source = scheduler.CreateColdObservable(
            OnNext(100, 1),
            OnNext(150, 2),
            OnNext(205, 3));

        var results = scheduler.CreateObserver<int>();

        source.LimitRate(TimeSpan.FromTicks(100), scheduler).Subscribe(results);

        scheduler.Start();

        results.Messages.AssertEqual(
            OnNext(100, 1),
            OnNext(200, 2),
            OnNext(300, 3));
    }

    [Test]
    public void MixedPacingWorks()
    {
        var scheduler = new TestScheduler();

        var source = scheduler.CreateColdObservable(
            OnNext(100, 1),
            OnNext(150, 1),
            OnNext(450, 3),
            OnNext(750, 4),
            OnNext(825, 5));

        var results = scheduler.CreateObserver<int>();

        source.LimitRate(TimeSpan.FromTicks(100), scheduler).Subscribe(results);

        scheduler.Start();

        results.Messages.AssertEqual(
            OnNext(100, 1),
            OnNext(450, 3),
            OnNext(750, 4),
            OnNext(850, 5));
    }
}

答案 1 :(得分:2)

您可以使用Observable.DistinctUntilChangedObservable.Sample

<强> Observable.DistinctUntilChanged

此方法仅在与前一个值不同时才会显示值。 (http://www.introtorx.com/content/v1.0.10621.0/05_Filtering.html

<强> Observable.Sample

Sample方法只为每个指定的TimeSpan取最后一个值。 (http://www.introtorx.com/content/v1.0.10621.0/13_TimeShiftedSequences.html#Sample

要生成所需效果,您可以将生成的第一个项目与上述项目结合起来。

答案 2 :(得分:2)

我意识到这已经有一段时间了,但是我想提供一种替代解决方案,我认为更准确地符合原始要求。该解决方案引入了2个自定义运算符。

首先是SampleImmediate,其工作原理与Sample完全相同,但它会立即发送第一个项目。这是通过许多操作员完成的。 Materialize / DematerializeDistinctUntilChanged一起工作以确保不会发送重复的通知。 MergeTake(1)Sample提供基本的“立即采样”功能。 PublishConnect将这些联系起来。 GroupBySelectMany确保我们在启动计时器之前等待第一个事件产生。 Create帮助我们妥善处理所有事情。

public static IObservable<T> SampleImmediate<T>(this IObservable<T> source, TimeSpan dueTime)
{
    return source
        .GroupBy(x => 0)
        .SelectMany(group =>
        {
            return Observable.Create<T>(o =>
            {
                var connectable = group.Materialize().Publish();

                var sub = Observable.Merge(
                        connectable.Sample(dueTime),
                        connectable.Take(1)
                    )
                    .DistinctUntilChanged()
                    .Dematerialize()
                    .Subscribe(o);

                return new CompositeDisposable(connectable.Connect(), sub);
            });
        });
}

在我们SampleImmediate之后,我们可以使用Cooldown创建GroupByUntil,以便在我们的滑动Throttle窗口关闭之前对发生的所有事件进行分组。一旦我们有了我们的小组,我们只需SampleImmediate整个小组。

public static IObservable<T> Cooldown<T>(this IObservable<T> source, TimeSpan dueTime)
{
    return source
        .GroupByUntil(x => 0, group => group.Throttle(dueTime))
        .SelectMany(group => group.SampleImmediate(dueTime));
}

我不建议这个解决方案是更好更快,我只是觉得看到另一种方法可能会很好。

答案 3 :(得分:0)

自我回答。

虽然我问Rx,但我的实际情况是它的端口(ReactiveCocoa)。更多的人都知道Rx,我可以翻译。

无论如何,我最终直接实现了它,以便它能够满足我想要的延迟/性能属性:

-(RACSignal*)cooldown:(NSTimeInterval)cooldownPeriod onScheduler:(RACScheduler *)scheduler {
    need(cooldownPeriod >= 0);
    need(!isnan(cooldownPeriod));
    need(scheduler != nil);
    need(scheduler != RACScheduler.immediateScheduler);

    force(cooldownPeriod != 0); //todo: bother with no-cooldown case?
    force(!isinf(cooldownPeriod)); //todo: bother with infinite case?

    return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
        need(subscriber != nil);

        NSObject* lock = [NSObject new];
        __block bool isCoolingDown = false;
        __block bool hasDelayedValue = false;
        __block id delayedValue = nil;
        __block RACDisposable *cooldownDisposer = nil;
        void (^onCanSendValue)(void) = ^{
            @synchronized (lock) {
                // check that we were actually cooling down
                // (e.g. what if the system thrashed before we could dispose the running-down timer, causing a redundant call?)
                if (!isCoolingDown) {
                    return;
                }

                // if no values arrived during the cooldown, we do nothing and can stop the timer for now
                if (!hasDelayedValue) {
                    isCoolingDown = false;
                    [cooldownDisposer dispose];
                    return;
                }

                // forward latest value
                id valueToSend = delayedValue;
                hasDelayedValue = false;
                delayedValue = nil;
                // todo: can this be avoided?
                // holding a lock while triggering arbitrary actions cam introduce subtle deadlock cases...
                [subscriber sendNext:valueToSend];
            }
        };
        void (^preemptivelyEndCooldown)(void) = ^{
            // forward latest value AND ALSO force cooldown to run out (disposing timer)
            onCanSendValue();
            onCanSendValue();
        };

        RACDisposable *selfDisposable = [self subscribeNext:^(id x) {
            bool didStartCooldown;
            @synchronized (lock) {
                hasDelayedValue = true;
                delayedValue = x;
                didStartCooldown = !isCoolingDown;
                isCoolingDown = true;
            }

            if (didStartCooldown) {
                // first item gets sent right away
                onCanSendValue();
                // coming items have to wait for the timer to run down
                cooldownDisposer = [[RACSignal interval:cooldownPeriod onScheduler:scheduler]
                                    subscribeNext:^(id _) { onCanSendValue(); }];
            }
        } error:^(NSError *error) {
            preemptivelyEndCooldown();
            [subscriber sendError:error];
        } completed:^{
            preemptivelyEndCooldown();
            [subscriber sendCompleted];
        }];

        return [RACDisposable disposableWithBlock:^{
            [selfDisposable dispose];
            @synchronized (lock) {
                isCoolingDown = false;
                [cooldownDisposer dispose];
            }
        }];
    }] setNameWithFormat:@"[%@ cooldown:%@]", self.name, @(cooldownPeriod)];
}

它应该几乎直接转换为.Net RX。当物品停止到达时,它将停止做任何工作,并且会在尊重冷却时尽快转发物品。