Rx for .Net:如何将Scan与Throttle结合起来

时间:2014-11-06 07:01:03

标签: c# .net events system.reactive reactive-programming

我的问题是:对于给定的事件序列,我想缓存它们的值,直到流中有暂停。然后,我将批量处理所有缓存数据并清除缓存状态。

这样做的一种天真的方法是(不是工作代码,可能存在一些错误):

struct FlaggedData
{
    public EventData Data { get; set; }
    public bool Reset { get; set; }
}

...

IObservable<EventData> eventsStream = GetStream();
var resetSignal = new Subject<FlaggedData>();

var flaggedDataStream = eventsStream
    .Select(data => new FlaggedData { Data = data })
    .Merge(resetSignal)
    .Scan(
        new List<EventData>(),
        (cache, flaggedData) =>
        {
            if (!flaggedData.Reset())
            {
                cache.Add(flaggedData.Data);
                return cache;
            }

            return new List<EventData>();
        })
    .Throttle(SomePeriodOfTime)
    .Subscribe(batch => 
        {
            resetSignal.OnNext(new FlaggedData { Reset = true});
            ProcessBatch(batch);
        });

所以在这里,在收到任何批处理之后,我请求重置缓存。问题是因为Throttle缓存中可能存在一些数据(或者我相信),在这种情况下会丢失。

我想要的是一些操作,如:

ScanWithThrottling<TAccumulate, TSource>(
    IObservable<TSource> source,
    Func<TAccumulate, TSource, TAccumulate> aggregate,
    TimeSpan throttlingSpan)

返回一个observable,它会在每次调用其订阅者的OnNext时重置累计值。

当然,我可以编写自己的扩展,但问题是我是否可以通过标准Rx操作实现相同的效果。

2 个答案:

答案 0 :(得分:4)

我认为这里有一个简单的方法。使用Buffer()根据节流缓冲元素,如下所示:

var buffered = source.Publish(ps =>        
    ps.Buffer(() => ps.Throttle(SomePeriodOfTime)));

这将缓冲元素,直到存在SomePeriodOfTime的间隙并将它们显示为列表。无需担心&#34;重置&#34;方面,你不会失去元素。

使用Publish可确保BufferThrottle可以使用的源事件的单个共享订阅。节流阀是缓冲器关闭功能,提供一个指示应启动新缓冲区的信号。

这是一个可测试的版本 - 我只是在这里转出每个缓冲区的长度并使用Timestamp添加时间信息,但它只是IList<T>你得到原始缓冲流。请注意如何将调度程序作为基于时间的操作的参数提供以启用测试。

注意,您需要nuget package rx-testing来运行此示例,以引入Rx测试框架并获取TestSchedulerReactiveTest类型:

void Main()
{
    var scenarios = new Scenarios();
    scenarios.Scenario1();
}

public class Scenarios : ReactiveTest
{
    public void Scenario1()
    {
        var scheduler = new TestScheduler();
        var source = scheduler.CreateHotObservable(
            OnNext(100, 1),
            OnNext(200, 2),
            OnNext(300, 3),
            OnNext(800, 4),
            OnNext(900, 5),
            OnNext(1400, 6),
            OnNext(1600, 7),
            OnNext(1700, 8),
            OnNext(1800, 9));

    var duration = TimeSpan.FromTicks(300);

    var buffered = source.Publish(ps =>        
        ps.Buffer(() => ps.Throttle(duration, scheduler)));

        buffered.Timestamp(scheduler).Subscribe(
            x => Console.WriteLine("Timestamp: {0} Value: {1}",
                 x.Timestamp.Ticks, x.Value.Count()));

        scheduler.Start();

    }
}

答案 1 :(得分:2)

基本上,似乎你想要一个缓冲而不是丢弃的Throttle

要解决这个问题,让我们考虑Throttle的工作原理:

  1. 当值到达时,将其缓存(替换之前的任何值)并启动计时器。
  2. 如果在计时器结束之前到达另一个值,请取消计时器并转到#1。
  3. 当计时器过去时,从缓存中推送值。
  4. 您的规范:

    1. 当值到达时,将其附加到缓冲区并启动计时器。
    2. 如果在计时器结束之前到达另一个值,请取消计时器并转到#1。
    3. 当计时器过去时,按下缓冲区并创建一个新的缓冲区。
    4. 因此:

      var throttled = source.Publish(p => 
        from value in p
        // TODO: Add to buffer (side effect)
        from _ in Observable.Timer(period, scheduler).TakeUntil(p)
        select buffer);  // TODO: Create new buffer (side effect + contention)
      

      如果指定的buffer引入了并发性,请注意竞争条件以及假设scheduler的可能争用。

      如果在Timer观察到新的pvalue过去了,那么在天真的实施中,最新的value可能会泄漏到buffer在推送buffer之前创建新的buffer。这可能是可以接受的,但无论你如何削减它都需要同步。除非您使用时间戳扩展通知或保证所有操作的线程关联性,否则我似乎无法获得您正在寻找的确切语义。

      无论如何,作为一个优秀的广义运营商,也许这是有意义的。这似乎正交到我作为一个Rx原语,除非我在我的分析中错了。考虑添加work item

      我还清楚地记得在Rx MSDN论坛中讨论类似的运算符,可能不止一次。可能值得寻找那些讨论。

      编辑:我已经改变了我在这个例子中使用同步的想法,因为无论如何,竞争条件在这里是不可避免的,假设scheduler引入了并发。

      未经测试的例子:

      public static IObservable<IList<TSource>> ThrottleBuffered<TSource>(
        this IObservable<TSource> source, 
        TimeSpan period,
        IScheduler scheduler)
      {
        return source.Publish(p => 
        {
          var buffer = new List<TSource>();
      
          return (from _ in p.Do(buffer.Add)
                  from __ in Observable.Timer(period, scheduler).TakeUntil(p)
                  .Do(_ => buffer = new List<TSource>())
                  select buffer);
        });
      }