C# Rx:我如何立即响应,并限制后续请求

C# Rx:我如何立即响应,并限制后续请求,c#,system.reactive,C#,System.reactive,我想设置一个可以立即响应事件的Rx订阅,然后忽略在指定的“冷却”时间内发生的后续事件 开箱即用的节流/缓冲方法只在超时时间过后才响应,这不是我所需要的 下面是一些设置场景并使用节流阀的代码(这不是我想要的解决方案): 类程序 { 静态秒表sw=新秒表(); 静态void Main(字符串[]参数) { var subject=新的subject(); var超时=TimeSpan.From毫秒(500); 主题 .油门(超时) .认购(DoStuff); var factory=new Task

我想设置一个可以立即响应事件的Rx订阅,然后忽略在指定的“冷却”时间内发生的后续事件

开箱即用的节流/缓冲方法只在超时时间过后才响应,这不是我所需要的

下面是一些设置场景并使用节流阀的代码(这不是我想要的解决方案):

类程序
{
静态秒表sw=新秒表();
静态void Main(字符串[]参数)
{
var subject=新的subject();
var超时=TimeSpan.From毫秒(500);
主题
.油门(超时)
.认购(DoStuff);
var factory=new TaskFactory();
sw.Start();
factory.StartNew(()=>
{
Console.WriteLine(“第1批(无延迟)”);
主题.OnNext(1);
});
工厂启动延时(1000,()=>
{
控制台写入线(“第2批(1s延迟)”;
主题.OnNext(2);
});
工厂启动延时(1300,()=>
{
控制台写入线(“第3批(1.3s延迟)”;
主题.OnNext(3);
});
工厂启动延时(1600,()=>
{
控制台写入线(“第4批(1.6s延迟)”;
主题.OnNext(4);
});
Console.ReadKey();
sw.Stop();
}
专用静态真空度计(int i)
{
WriteLine(“在{1}毫秒处处理{0}”,i,sw.elapsedmillesons);
}
}
立即运行此操作的输出为:

第一批(不得延误)

以508ms处理1

第2批(1s延迟)

第3批(延迟1.3s)

第4批(延迟1.6s)

在2114ms时处理4

请注意,批处理2没有被处理(这很好!),因为由于节流的性质,请求之间需要等待500毫秒。第3批由于靠近第4批,因此也未处理(这不太正常,因为它发生在距第2批500ms的地方)

我要找的是更像这样的东西:

第一批(不得延误)

以~0毫秒的速度处理1

第2批(1s延迟)

在约1000秒时处理2次

第3批(延迟1.3s)

第4批(延迟1.6s)

在约1600秒时处理4次

请注意,在这种情况下不会处理第3批(这很好!),因为它发生在第2批的500毫秒之内

编辑

下面是我使用的“StartNewDelayed”扩展方法的实现:

/// <summary>Creates a Task that will complete after the specified delay.</summary>
/// <param name="factory">The TaskFactory.</param>
/// <param name="millisecondsDelay">The delay after which the Task should transition to RanToCompletion.</param>
/// <returns>A Task that will be completed after the specified duration.</returns>
public static Task StartNewDelayed(
    this TaskFactory factory, int millisecondsDelay)
{
    return StartNewDelayed(factory, millisecondsDelay, CancellationToken.None);
}

/// <summary>Creates a Task that will complete after the specified delay.</summary>
/// <param name="factory">The TaskFactory.</param>
/// <param name="millisecondsDelay">The delay after which the Task should transition to RanToCompletion.</param>
/// <param name="cancellationToken">The cancellation token that can be used to cancel the timed task.</param>
/// <returns>A Task that will be completed after the specified duration and that's cancelable with the specified token.</returns>
public static Task StartNewDelayed(this TaskFactory factory, int millisecondsDelay, CancellationToken cancellationToken)
{
    // Validate arguments
    if (factory == null) throw new ArgumentNullException("factory");
    if (millisecondsDelay < 0) throw new ArgumentOutOfRangeException("millisecondsDelay");

    // Create the timed task
    var tcs = new TaskCompletionSource<object>(factory.CreationOptions);
    var ctr = default(CancellationTokenRegistration);

    // Create the timer but don't start it yet.  If we start it now,
    // it might fire before ctr has been set to the right registration.
    var timer = new Timer(self =>
    {
        // Clean up both the cancellation token and the timer, and try to transition to completed
        ctr.Dispose();
        ((Timer)self).Dispose();
        tcs.TrySetResult(null);
    });

    // Register with the cancellation token.
    if (cancellationToken.CanBeCanceled)
    {
        // When cancellation occurs, cancel the timer and try to transition to cancelled.
        // There could be a race, but it's benign.
        ctr = cancellationToken.Register(() =>
        {
            timer.Dispose();
            tcs.TrySetCanceled();
        });
    }

    if (millisecondsDelay > 0)
    {
        // Start the timer and hand back the task...
        timer.Change(millisecondsDelay, Timeout.Infinite);
    }
    else
    {
        // Just complete the task, and keep execution on the current thread.
        ctr.Dispose();
        tcs.TrySetResult(null);
        timer.Dispose();
    }

    return tcs.Task;
}
///创建将在指定延迟后完成的任务。
///工厂。
///任务应过渡到RANTO完成的延迟。
///将在指定的持续时间后完成的任务。
公共静态任务StartNewDelayed(
此TaskFactory工厂,int毫秒显示)
{
返回StartNewDelayed(工厂、毫秒显示、取消令牌。无);
}
///创建将在指定延迟后完成的任务。
///工厂。
///任务应过渡到RANTO完成的延迟。
///可用于取消定时任务的取消令牌。
///将在指定的持续时间后完成的任务,可使用指定的令牌取消。
公共静态任务StartNewDelayed(此TaskFactory工厂,int毫秒显示,CancellationToken CancellationToken)
{
//验证参数
如果(factory==null)抛出新的ArgumentNullException(“factory”);
如果(毫秒显示<0)抛出新ArgumentOutOfRangeException(“毫秒显示”);
//创建定时任务
var tcs=新任务完成源(factory.CreationOptions);
var ctr=默认值(CancellationTokenRegistration);
//创建计时器,但不要启动。如果现在启动,
//它可能在ctr设置为正确注册之前触发。
变量计时器=新计时器(自=>
{
//清除取消令牌和计时器,并尝试转换到已完成
ctr.Dispose();
((计时器)self.Dispose();
tcs.TrySetResult(空);
});
//使用取消令牌注册。
如果(cancellationToken.canbecancelled)
{
//取消时,取消计时器并尝试转换到取消。
//可能会有比赛,但这是良性的。
ctr=cancellationToken.Register(()=>
{
timer.Dispose();
tcs.trysetconceled();
});
}
如果(毫秒显示>0)
{
//启动计时器并返回任务。。。
timer.Change(毫秒显示,超时无限);
}
其他的
{
//只需完成任务,并在当前线程上继续执行。
ctr.Dispose();
tcs.TrySetResult(空);
timer.Dispose();
}
返回tcs.Task;
}

经过多次尝试和错误后,我找到的解决方案是用以下内容替换受限制的订阅:

subject
    .Window(() => { return Observable.Interval(timeout); })
    .SelectMany(x => x.Take(1))
    .Subscribe(i => DoStuff(i));

编辑以合并保罗的清理。

很棒的解决方案安德鲁!不过,我们可以更进一步,清理内部订阅:

subject
    .Window(() => { return Observable.Interval(timeout); })
    .SelectMany(x => x.Take(1))
    .Subscribe(DoStuff);

我发布的最初答案有一个缺陷:即
Window
方法与
Observable.Interval
一起使用时,表示窗口结束,设置了一个500毫秒的无限系列窗口。我真正需要的是一个窗口,当第一个结果被注入主体时开始,500毫秒后结束

我的示例数据掩盖了这个问题,因为数据很好地分解到了即将创建的窗口中。(即0-500ms、501-1000ms、1001-1500ms等)

相反,请考虑以下时机:

factory.StartNewDelayed(300,() =>
{
    Console.WriteLine("Batch 1 (300ms delay)");
    subject.OnNext(1);
});

factory.StartNewDelayed(700, () =>
{
    Console.WriteLine("Batch 2 (700ms delay)");
    subject.OnNext(2);
});

factory.StartNewDelayed(1300, () =>
{
    Console.WriteLine("Batch 3 (1.3s delay)");
    subject.OnNext(3);
});

factory.StartNewDelayed(1600, () =>
{
    Console.WriteLine("Batch 4 (1.6s delay)");
    subject.OnNext(4);
});
我得到的是:

第1批(延迟300毫秒)

在356ms时处理1

第2批(延迟700ms)

在750ms时处理2

factory.StartNewDelayed(300,() => { Console.WriteLine("Batch 1 (300ms delay)"); subject.OnNext(1); }); factory.StartNewDelayed(700, () => { Console.WriteLine("Batch 2 (700ms delay)"); subject.OnNext(2); }); factory.StartNewDelayed(1300, () => { Console.WriteLine("Batch 3 (1.3s delay)"); subject.OnNext(3); }); factory.StartNewDelayed(1600, () => { Console.WriteLine("Batch 4 (1.6s delay)"); subject.OnNext(4); });

bool isCoolingDown = false;

subject
    .Where(_ => !isCoolingDown)
    .Subscribe(
    i =>
    {
        DoStuff(i);

        isCoolingDown = true;

        Observable
            .Interval(cooldownInterval)
            .Take(1)
            .Subscribe(_ => isCoolingDown = false);
    });
subject
    .Take(1)
    .Concat(Observable.Empty<long>().Delay(TimeSpan.FromMilliseconds(500)))
    .Repeat();
subject
    .Window(() => Observable.Timer(TimeSpan.FromMilliseconds(500)))
    .SelectMany(x => x.Take(1));
public static class ObservableExtensions
{
    public static IObservable<T> SampleFirst<T>(
        this IObservable<T> source,
        TimeSpan sampleDuration,
        IScheduler scheduler = null)
    {
        scheduler = scheduler ?? Scheduler.Default;
        return source.Publish(ps => 
            ps.Window(() => ps.Delay(sampleDuration,scheduler))
              .SelectMany(x => x.Take(1)));
    }
}
public static class ObservableExtensions
{
    public static IObservable<T> SampleFirst<T>(
        this IObservable<T> source,
        TimeSpan sampleDuration,
        IScheduler scheduler = null)
    {
        scheduler = scheduler ?? Scheduler.Default;
        var sourcePub = source.Publish().RefCount();
        return sourcePub.Window(() => sourcePub.Delay(sampleDuration,scheduler))
                        .SelectMany(x => x.Take(1));
    }
}
public static IObservable<T> QuickThrottle<T>(this IObservable<T> src, TimeSpan interval, IScheduler scheduler)
{
  return src
    .Scan(new ValueAndDueTime<T>(), (prev, id) => AccumulateForQuickThrottle(prev, id, interval, scheduler))
    .Where(vd => !vd.Ignore)
    .SelectMany(sc => Observable.Timer(sc.DueTime, scheduler).Select(_ => sc.Value));
}

private static ValueAndDueTime<T> AccumulateForQuickThrottle<T>(ValueAndDueTime<T> prev, T value, TimeSpan interval, IScheduler s)
{
  var now = s.Now;

  // Ignore this completely if there is already a future item scheduled
  //  but do keep the dueTime for accumulation!
  if (prev.DueTime > now) return new ValueAndDueTime<T> { DueTime = prev.DueTime, Ignore = true };

  // Schedule this item at at least interval from the previous
  var min = prev.DueTime + interval;
  var nextTime = (now < min) ? min : now;
  return new ValueAndDueTime<T> { DueTime = nextTime, Value = value };
}

private class ValueAndDueTime<T>
{
  public DateTimeOffset DueTime;
  public T Value;
  public bool Ignore;
}
public static IObservable<T> ThrottleOrImmediate<T>(this IObservable<T> source, TimeSpan delay, IScheduler scheduler)
{
    return Observable.Create<T>((obs, token) =>
    {
        // Next item cannot be send before that time
        DateTime nextItemTime = default;

        return Task.FromResult(source.Subscribe(async item =>
        {
            var currentTime = DateTime.Now;
            // If we already reach the next item time
            if (currentTime - nextItemTime >= TimeSpan.Zero)
            {
                // Following item will be send only after the set delay
                nextItemTime = currentTime + delay;
                // send current item with scheduler
                scheduler.Schedule(() => obs.OnNext(item));
            }
            // There is still time before we can send an item
            else
            {
                // we schedule the time for the following item
                nextItemTime = currentTime + delay;
                try
                {
                    await Task.Delay(delay, token);
                }
                catch (TaskCanceledException)
                {
                    return;
                }

                // If next item schedule was change by another item then we stop here
                if (nextItemTime > currentTime + delay)
                    return;
                else
                {
                    // Set next possible time for an item and send item with scheduler
                    nextItemTime = currentTime + delay;
                    scheduler.Schedule(() => obs.OnNext(item));
                }
            }
        }));

    });
}