51

我正在收听硬件事件消息,但我需要对其进行去抖动以避免太多查询。

这是一个发送机器状态的硬件事件,我必须将其存储在数据库中以用于统计目的,有时它的状态会经常变化(闪烁?)。在这种情况下,我只想存储一个“稳定”状态,我想通过在将状态存储到数据库之前等待 1-2 秒来实现它。

这是我的代码:

private MachineClass connect()
{
    try
    {
        MachineClass rpc = new MachineClass();
        rpc.RxVARxH += eventRxVARxH;
        return rpc;
    }
    catch (Exception e1)
    {
        log.Error(e1.Message);
        return null;
    }
}

private void eventRxVARxH(MachineClass Machine)
{
    log.Debug("Event fired");
}

我将这种行为称为“去抖动”:等待几次以真正完成它的工作:如果在去抖动时间内再次触发相同的事件,我必须关闭第一个请求并开始等待去抖动时间以完成第二个事件。

管理它的最佳选择是什么?只是一个一次性计时器?

要解释“去抖动”功能,请查看关键事件的这个 javascript 实现:http: //benalman.com/code/projects/jquery-throttle-debounce/examples/debounce/

4

18 回答 18

51

我用它来消除事件并取得了一些成功:

public static Action<T> Debounce<T>(this Action<T> func, int milliseconds = 300)
{
    var last = 0;
    return arg =>
    {
        var current = Interlocked.Increment(ref last);
        Task.Delay(milliseconds).ContinueWith(task =>
        {
            if (current == last) func(arg);
            task.Dispose();
        });
    };
}

用法

Action<int> a = (arg) =>
{
    // This was successfully debounced...
    Console.WriteLine(arg);
};
var debouncedWrapper = a.Debounce<int>();

while (true)
{
    var rndVal = rnd.Next(400);
    Thread.Sleep(rndVal);
    debouncedWrapper(rndVal);
}

它可能不像 RX 中的那样强大,但它很容易理解和使用。

跟进 2020-02-03

使用取消令牌修改了@collie 的解决方案,如下所示

public static Action<T> Debounce<T>(this Action<T> func, int milliseconds = 300)
{
    CancellationTokenSource? cancelTokenSource = null;

    return arg =>
    {
        cancelTokenSource?.Cancel();
        cancelTokenSource = new CancellationTokenSource();

        Task.Delay(milliseconds, cancelTokenSource.Token)
            .ContinueWith(t =>
            {
                if (t.IsCompletedSuccessfully)
                {
                    func(arg);
                }
            }, TaskScheduler.Default);
    };
}

笔记:

  • 调用Cancel足以处理 CTS
  • 成功完成的 CTS 直到下一次调用才会取消/处置
  • 正如@collie 所指出的,任务被释放,因此无需调用Dispose任务

我以前没有使用过取消令牌,可能没有正确使用它们。

于 2015-04-07T12:44:12.340 回答
50

这不是一个从头开始编码的简单请求,因为有几个细微差别。一个类似的场景是监视 FileSystemWatcher 并等待在大副本之后安静下来,然后再尝试打开修改后的文件。

.NET 4.5 中的响应式扩展正是为了处理这些场景而创建的。您可以通过ThrottleBufferWindowSample等方法轻松地使用它们来提供此类功能。您将事件发布到Subject,对其应用窗口函数之一,例如,仅当 X 秒或 Y 事件没有活动时才获取通知,然后订阅通知。

Subject<MyEventData> _mySubject=new Subject<MyEventData>();
....
var eventSequenc=mySubject.Throttle(TimeSpan.FromSeconds(1))
                          .Subscribe(events=>MySubscriptionMethod(events));

只有当窗口中没有其他事件时,Throttle 才会返回滑动窗口中的最后一个事件。任何事件都会重置窗口。

您可以在此处找到有关时移函数的非常好的概述

当你的代码接收到事件时,你只需要使用 OnNext 将其发布到 Subject 即可:

_mySubject.OnNext(MyEventData);

如果您的硬件事件表现为典型的 .NET 事件,您可以绕过主题并使用Observable.FromEventPattern手动发布,如下所示

var mySequence = Observable.FromEventPattern<MyEventData>(
    h => _myDevice.MyEvent += h,
    h => _myDevice.MyEvent -= h);  
_mySequence.Throttle(TimeSpan.FromSeconds(1))
           .Subscribe(events=>MySubscriptionMethod(events));

您还可以从 Tasks 创建 observables,将事件序列与 LINQ 运算符组合以请求例如:使用 Zip 的不同硬件事件对,使用另一个事件源来绑定 Throttle/Buffer 等,添加延迟等等。

Reactive Extensions 以NuGet 包的形式提供,因此很容易将它们添加到您的项目中。

Stephen Cleary 的书“ Concurrency in C# Cookbook ”是关于 Reactive Extensions 的非常好的资源,并解释了如何使用它以及它如何与 .NET 中的其他并发 API(如任务、事件等)相匹配。

Introduction to Rx是一个优秀的系列文章(我从那里复制了示例),其中包含几个示例。

更新

使用您的具体示例,您可以执行以下操作:

IObservable<MachineClass> _myObservable;

private MachineClass connect()
{

    MachineClass rpc = new MachineClass();
   _myObservable=Observable
                 .FromEventPattern<MachineClass>(
                            h=> rpc.RxVARxH += h,
                            h=> rpc.RxVARxH -= h)
                 .Throttle(TimeSpan.FromSeconds(1));
   _myObservable.Subscribe(machine=>eventRxVARxH(machine));
    return rpc;
}

当然,这可以大大改善 - observable 和订阅都需要在某个时候处理。此代码假定您只控制一个设备。如果您有很多设备,您可以在类中创建 observable,以便每个 MachineClass 公开和处置自己的 observable。

于 2015-02-12T08:21:26.980 回答
10

最近,我正在对一个针对旧版本 .NET 框架 (v3.5) 的应用程序进行一些维护。

我不能使用 Reactive Extensions 也不能使用 Task Parallel Library,但我需要一种很好、干净、一致的去抖动事件的方法。这是我想出的:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

namespace MyApplication
{
    public class Debouncer : IDisposable
    {
        readonly TimeSpan _ts;
        readonly Action _action;
        readonly HashSet<ManualResetEvent> _resets = new HashSet<ManualResetEvent>();
        readonly object _mutex = new object();

        public Debouncer(TimeSpan timespan, Action action)
        {
            _ts = timespan;
            _action = action;
        }

        public void Invoke()
        {
            var thisReset = new ManualResetEvent(false);

            lock (_mutex)
            {
                while (_resets.Count > 0)
                {
                    var otherReset = _resets.First();
                    _resets.Remove(otherReset);
                    otherReset.Set();
                }

                _resets.Add(thisReset);
            }

            ThreadPool.QueueUserWorkItem(_ =>
            {
                try
                {
                    if (!thisReset.WaitOne(_ts))
                    {
                        _action();
                    }
                }
                finally
                {
                    lock (_mutex)
                    {
                        using (thisReset)
                            _resets.Remove(thisReset);
                    }
                }
            });
        }

        public void Dispose()
        {
            lock (_mutex)
            {
                while (_resets.Count > 0)
                {
                    var reset = _resets.First();
                    _resets.Remove(reset);
                    reset.Set();
                }
            }
        }
    }
}

以下是在具有搜索文本框的 Windows 窗体中使用它的示例:

public partial class Example : Form 
{
    private readonly Debouncer _searchDebouncer;

    public Example()
    {
        InitializeComponent();
        _searchDebouncer = new Debouncer(TimeSpan.FromSeconds(.75), Search);
        txtSearchText.TextChanged += txtSearchText_TextChanged;
    }

    private void txtSearchText_TextChanged(object sender, EventArgs e)
    {
        _searchDebouncer.Invoke();
    }

    private void Search()
    {
        if (InvokeRequired)
        {
            Invoke((Action)Search);
            return;
        }

        if (!string.IsNullOrEmpty(txtSearchText.Text))
        {
            // Search here
        }
    }
}
于 2017-07-14T19:02:35.203 回答
10

我遇到了这个问题。我在这里尝试了每个答案,并且由于我使用的是 Xamarin 通用应用程序,因此我似乎缺少每个答案中所需的某些内容,并且我不想添加任何更多的包或库。我的解决方案完全按照我的预期工作,而且我没有遇到任何问题。希望它可以帮助某人。

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace OrderScanner.Models
{
    class Debouncer
    {
        private List<CancellationTokenSource> StepperCancelTokens = new List<CancellationTokenSource>();
        private int MillisecondsToWait;
        private readonly object _lockThis = new object(); // Use a locking object to prevent the debouncer to trigger again while the func is still running

        public Debouncer(int millisecondsToWait = 300)
        {
            this.MillisecondsToWait = millisecondsToWait;
        }

        public void Debouce(Action func)
        {
            CancelAllStepperTokens(); // Cancel all api requests;
            var newTokenSrc = new CancellationTokenSource();
            lock (_lockThis)
            {
                StepperCancelTokens.Add(newTokenSrc);
            }
            Task.Delay(MillisecondsToWait, newTokenSrc.Token).ContinueWith(task => // Create new request
            {
                if (!newTokenSrc.IsCancellationRequested) // if it hasn't been cancelled
                {
                    CancelAllStepperTokens(); // Cancel any that remain (there shouldn't be any)
                    StepperCancelTokens = new List<CancellationTokenSource>(); // set to new list
                    lock (_lockThis)
                    {
                        func(); // run
                    }
                }
            }, TaskScheduler.FromCurrentSynchronizationContext());
        }

        private void CancelAllStepperTokens()
        {
            foreach (var token in StepperCancelTokens)
            {
                if (!token.IsCancellationRequested)
                {
                    token.Cancel();
                }
            }
        }
    }
}

是这样称呼的...

private Debouncer StepperDeboucer = new Debouncer(1000); // one second

StepperDeboucer.Debouce(() => { WhateverMethod(args) });

对于机器每秒可以发送数百个请求的任何事情,我不会推荐这个,但是对于用户输入,它工作得很好。我在一个 android/IOS 应用程序的步进器上使用它,该应用程序在一步调用 api。

于 2017-12-21T22:28:09.673 回答
7

RX 可能是最简单的选择,尤其是当您已经在应用程序中使用它时。但如果没有,添加它可能有点矫枉过正。

对于基于 UI 的应用程序(如 WPF),我使用以下使用 DispatcherTimer 的类:

public class DebounceDispatcher
{
    private DispatcherTimer timer;
    private DateTime timerStarted { get; set; } = DateTime.UtcNow.AddYears(-1);

    public void Debounce(int interval, Action<object> action,
        object param = null,
        DispatcherPriority priority = DispatcherPriority.ApplicationIdle,
        Dispatcher disp = null)
    {
        // kill pending timer and pending ticks
        timer?.Stop();
        timer = null;

        if (disp == null)
            disp = Dispatcher.CurrentDispatcher;

        // timer is recreated for each event and effectively
        // resets the timeout. Action only fires after timeout has fully
        // elapsed without other events firing in between
        timer = new DispatcherTimer(TimeSpan.FromMilliseconds(interval), priority, (s, e) =>
        {
            if (timer == null)
                return;

            timer?.Stop();
            timer = null;
            action.Invoke(param);
        }, disp);

        timer.Start();
    }
}

要使用它:

private DebounceDispatcher debounceTimer = new DebounceDispatcher();

private void TextSearchText_KeyUp(object sender, KeyEventArgs e)
{
    debounceTimer.Debounce(500, parm =>
    {
        Model.AppModel.Window.ShowStatus("Searching topics...");
        Model.TopicsFilter = TextSearchText.Text;
        Model.AppModel.Window.ShowStatus();
    });
}

现在仅在键盘空闲 200 毫秒后才处理键事件 - 任何先前的未决事件都将被丢弃。

还有一个 Throttle 方法,它总是在给定的时间间隔后触发事件:

    public void Throttle(int interval, Action<object> action,
        object param = null,
        DispatcherPriority priority = DispatcherPriority.ApplicationIdle,
        Dispatcher disp = null)
    {
        // kill pending timer and pending ticks
        timer?.Stop();
        timer = null;

        if (disp == null)
            disp = Dispatcher.CurrentDispatcher;

        var curTime = DateTime.UtcNow;

        // if timeout is not up yet - adjust timeout to fire 
        // with potentially new Action parameters           
        if (curTime.Subtract(timerStarted).TotalMilliseconds < interval)
            interval = (int) curTime.Subtract(timerStarted).TotalMilliseconds;

        timer = new DispatcherTimer(TimeSpan.FromMilliseconds(interval), priority, (s, e) =>
        {
            if (timer == null)
                return;

            timer?.Stop();
            timer = null;
            action.Invoke(param);
        }, disp);

        timer.Start();
        timerStarted = curTime;            
    }
于 2017-07-02T23:47:11.213 回答
5

Panagiotis 的回答当然是正确的,但是我想举一个更简单的例子,因为我花了一段时间来整理如何让它工作。我的场景是用户在搜索框中输入内容,当用户输入内容时,我们希望调用 api 来返回搜索建议,因此我们希望对 api 调用进行去抖动处理,这样他们就不会在每次输入字符时都进行一次调用。

我正在使用 Xamarin.Android,但这应该适用于任何 C# 场景......

private Subject<string> typingSubject = new Subject<string> ();
private IDisposable typingEventSequence;

private void Init () {
            var searchText = layoutView.FindViewById<EditText> (Resource.Id.search_text);
            searchText.TextChanged += SearchTextChanged;
            typingEventSequence = typingSubject.Throttle (TimeSpan.FromSeconds (1))
                .Subscribe (query => suggestionsAdapter.Get (query));
}

private void SearchTextChanged (object sender, TextChangedEventArgs e) {
            var searchText = layoutView.FindViewById<EditText> (Resource.Id.search_text);
            typingSubject.OnNext (searchText.Text.Trim ());
        }

public override void OnDestroy () {
            if (typingEventSequence != null)
                typingEventSequence.Dispose ();
            base.OnDestroy ();
        }

当你第一次初始化屏幕/类时,你创建了你的事件来监听用户输入(SearchTextChanged),然后还设置了一个限制订阅,它与“typingSubject”相关联。

接下来,在 SearchTextChanged 事件中,您可以调用 typingSubject.OnNext 并传入搜索框的文本。在去抖周期(1 秒)之后,它会调用订阅的事件(在我们的例子中是suggestionsAdapter.Get。)

最后,当屏幕关闭时,请务必处理订阅!

于 2016-08-29T16:26:44.140 回答
4

我需要这样的东西,但在一个网络应用程序中,所以我不能将它存储Action在一个变量中,它会在 http 请求之间丢失。

基于其他答案和@Collie 的想法,我创建了一个查看用于限制的唯一字符串键的类。

public static class Debouncer
{
    static ConcurrentDictionary<string, CancellationTokenSource> _tokens = new ConcurrentDictionary<string, CancellationTokenSource>();
    public static void Debounce(string uniqueKey, Action action, int seconds)
    {
        var token = _tokens.AddOrUpdate(uniqueKey,
            (key) => //key not found - create new
            {
                return new CancellationTokenSource();
            },
            (key, existingToken) => //key found - cancel task and recreate
            {
                existingToken.Cancel(); //cancel previous
                return new CancellationTokenSource();
            }
        );

        Task.Delay(seconds * 1000, token.Token).ContinueWith(task =>
        {
            if (!task.IsCanceled)
            {
                action();
                _tokens.TryRemove(uniqueKey, out _);
            }
        }, token.Token);
    }
}

用法:

//throttle for 5 secs if it's already been called with this KEY
Debouncer.Debounce("Some-Unique-ID", () => SendEmails(), 5);

作为附带奖励,因为它基于字符串键,您可以使用内联lambda

Debouncer.Debounce("Some-Unique-ID", () => 
{
    //do some work here
}, 5);
于 2020-11-17T00:37:52.837 回答
3

这个小宝石的灵感来自 Mike Wards 恶魔般巧妙的扩展尝试。然而,这个很好地清理了自己。

public static Action Debounce(this Action action, int milliseconds = 300)
{
    CancellationTokenSource lastCToken = null;

    return () =>
    {
        //Cancel/dispose previous
        lastCToken?.Cancel();
        try { 
            lastCToken?.Dispose(); 
        } catch {}          

        var tokenSrc = lastCToken = new CancellationTokenSource();

        Task.Delay(milliseconds).ContinueWith(task => { action(); }, tokenSrc.Token);
    };
}

注意:在这种情况下不需要处理任务。有关证据,请参见此处

用法

Action DebounceToConsole;
int count = 0;

void Main()
{
    //Assign
    DebounceToConsole = ((Action)ToConsole).Debounce(50);

    var random = new Random();
    for (int i = 0; i < 50; i++)
    {
        DebounceToConsole();
        Thread.Sleep(random.Next(100));
    }
}

public void ToConsole()
{
    Console.WriteLine($"I ran for the {++count} time.");
}
于 2019-12-12T02:42:36.757 回答
3

创建这个类来解决它也为等待调用:

public class Debouncer
{
    private CancellationTokenSource _cancelTokenSource = null;

    public async Task Debounce(Func<Task> method, int milliseconds = 300)
    {
        _cancelTokenSource?.Cancel();
        _cancelTokenSource?.Dispose();

        _cancelTokenSource = new CancellationTokenSource();

        await Task.Delay(milliseconds, _cancelTokenSource.Token);

        await method();
    }
}

使用示例:

private Debouncer _debouncer = new Debouncer();
....
await _debouncer.Debounce(YourAwaitableMethod);
于 2021-10-06T03:07:32.127 回答
2

这是受 Nieminen 的 Task.Delay-based Debouncer启发的。简化,一些小的更正,并且应该更好地自行清理。

class Debouncer: IDisposable
{
    private CancellationTokenSource lastCToken;
    private int milliseconds;

    public Debouncer(int milliseconds = 300)
    {
        this.milliseconds = milliseconds;
    }

    public void Debounce(Action action)
    {
        Cancel(lastCToken);

        var tokenSrc = lastCToken = new CancellationTokenSource();

        Task.Delay(milliseconds).ContinueWith(task =>
        {
             action();
        }, 
            tokenSrc.Token
        );
    }

    public void Cancel(CancellationTokenSource source)
    {
        if (source != null)
        {
            source.Cancel();
            source.Dispose();
        }                 
    }

    public void Dispose()
    {
        Cancel(lastCToken);
    }

    ~Debouncer()
    {
        Dispose();
    }
}

用法

private Debouncer debouncer = new Debouncer(500); //1/2 a second
...
debouncer.Debounce(SomeAction);
于 2019-12-12T04:35:04.490 回答
2

我需要 Blazor 的 Debounce 方法并不断返回此页面,因此我想分享我的解决方案,以防它帮助其他人。

public class DebounceHelper
{
    private CancellationTokenSource debounceToken = null;

    public async Task DebounceAsync(Func<CancellationToken, Task> func, int milliseconds = 1000)
    {
        try
        {
            // Cancel previous task
            if (debounceToken != null) { debounceToken.Cancel(); }

            // Assign new token
            debounceToken = new CancellationTokenSource();

            // Debounce delay
            await Task.Delay(milliseconds, debounceToken.Token);

            // Throw if canceled
            debounceToken.Token.ThrowIfCancellationRequested();

            // Run function
            await func(debounceToken.Token);
        }
        catch (TaskCanceledException) { }
    }
}

对搜索功能的示例调用

<input type="text" @oninput=@(async (eventArgs) => await OnSearchInput(eventArgs)) />

@code {
    private readonly DebounceHelper debouncer = new DebounceHelper();

    private async Task OnSearchInput(ChangeEventArgs eventArgs)
    {
        await debouncer.DebounceAsync(async (cancellationToken) =>
        {
            // Search Code Here         
        });
    }
}

于 2020-06-04T14:03:51.393 回答
1

只需记住最新的“热门歌曲:

DateTime latestHit = DatetIme.MinValue;

private void eventRxVARxH(MachineClass Machine)
{
    log.Debug("Event fired");
    if(latestHit - DateTime.Now < TimeSpan.FromXYZ() // too fast
    {
        // ignore second hit, too fast
        return;
    }
    latestHit = DateTime.Now;
    // it was slow enough, do processing
    ...
}

如果在最后一个事件之后有足够的时间,这将允许第二个事件。

请注意:不可能(以简单的方式)处理一系列快速事件中的最后一个事件,因为您永远不知道哪个是最后一个事件......

...除非您准备好处理很久以前的最后一个突发事件。然后你必须记住最后一个事件并在下一个事件足够慢时记录它:

DateTime latestHit = DatetIme.MinValue;
Machine historicEvent;

private void eventRxVARxH(MachineClass Machine)
{
    log.Debug("Event fired");

    if(latestHit - DateTime.Now < TimeSpan.FromXYZ() // too fast
    {
        // ignore second hit, too fast
        historicEvent = Machine; // or some property
        return;
    }
    latestHit = DateTime.Now;
    // it was slow enough, do processing
    ...
    // process historicEvent
    ...
    historicEvent = Machine; 
}
于 2015-02-12T08:10:22.860 回答
0

我在我的类定义中提出了这个。

如果在此时间段(示例中为 3 秒)内没有任何操作,我想立即运行我的操作。

如果在最后三秒内发生了什么事,我想发送在那段时间内发生的最后一件事。

    private Task _debounceTask = Task.CompletedTask;
    private volatile Action _debounceAction;

    /// <summary>
    /// Debounces anything passed through this 
    /// function to happen at most every three seconds
    /// </summary>
    /// <param name="act">An action to run</param>
    private async void DebounceAction(Action act)
    {
        _debounceAction = act;
        await _debounceTask;

        if (_debounceAction == act)
        {
            _debounceTask = Task.Delay(3000);
            act();
        }
    }

所以,如果我将时钟细分为每一刻钟

  TIME:  1e&a2e&a3e&a4e&a5e&a6e&a7e&a8e&a9e&a0e&a
  EVENT:  A         B    C   D  E              F  
OBSERVED: A           B           E            F

请注意,不会尝试提前取消任务,因此操作可能会堆积 3 秒,然后最终可用于垃圾收集。

于 2018-09-28T12:48:48.110 回答
0

想出了如何使用 System.Reactive NuGet 包在 TextBox 上进行适当的去抖动。

在班级层面,我们有自己的领域

private IObservable<EventPattern<TextChangedEventArgs>> textChanged;

然后当我们要开始监听事件时:

// Debouncing capability
textChanged = Observable.FromEventPattern<TextChangedEventArgs>(txtSearch, "TextChanged");
textChanged.ObserveOnDispatcher().Throttle(TimeSpan.FromSeconds(1)).Subscribe(args => {
    Debug.WriteLine("bounce!");
});

确保您没有将文本框连接到事件处理程序。上面的 Lambda 是事件处理程序。

于 2020-08-26T22:05:07.727 回答
0

我写了一个不运行异步同步的异步去抖动器。

public sealed class Debouncer : IDisposable {

  public Debouncer(TimeSpan? delay) => _delay = delay ?? TimeSpan.FromSeconds(2);

  private readonly TimeSpan _delay;
  private CancellationTokenSource? previousCancellationToken = null;

  public async Task Debounce(Action action) {
    _ = action ?? throw new ArgumentNullException(nameof(action));
    Cancel();
    previousCancellationToken = new CancellationTokenSource();
    try {
      await Task.Delay(_delay, previousCancellationToken.Token);
      await Task.Run(action, previousCancellationToken.Token);
    }
    catch (TaskCanceledException) { }    // can swallow exception as nothing more to do if task cancelled
  }

  public void Cancel() {
    if (previousCancellationToken != null) {
      previousCancellationToken.Cancel();
      previousCancellationToken.Dispose();
    }
  }

  public void Dispose() => Cancel();

}

我用它来消除文件更改报告的更改,请参见此处的完整示例。

于 2021-03-05T11:14:26.200 回答
0

我受到 Mike 的回答的启发,但需要无需任务即可工作的解决方案,它只会吞噬后续事件调用,直到去抖动超时用完。这是我的解决方案:

public static Action<T> Debounce<T>(this Action<T> action, int milliseconds = 300)
{
    DateTime? runningCallTime = null;
    var locker = new object();

    return arg =>
    {
        lock (locker)
        {
            if (!runningCallTime.HasValue ||
                runningCallTime.Value.AddMilliseconds(milliseconds) <= DateTime.UtcNow)
            {
                runningCallTime = DateTime.UtcNow;
                action.Invoke(arg);
            }
        }
    };

}
于 2021-06-22T11:15:16.937 回答
0

我根据@Mike Ward 的回答做了一些更简单的解决方案:

public static class CustomTaskExtension
{
    #region fields

    private static int _last = 0;

    #endregion

    public static void Debounce(CancellationTokenSource throttleCts, double debounceTimeMs, Action action)
    {
        var current = Interlocked.Increment(ref _last);
        Task.Delay(TimeSpan.FromMilliseconds(debounceTimeMs), throttleCts.Token)
            .ContinueWith(task =>
            {
                if (current == _last) action();
                task.Dispose();
            });
    }
}

示例如何使用它:

// security way to cancel the debounce process any time
CancellationTokenSource _throttleCts = new CancellationTokenSource();

public void MethodCalledManyTimes()
{
    // will wait 250ms after the last call
    CustomTaskExtension.Debounce(_throttleCts, 250, async () =>
    {
        Console.Write("Execute your code 250ms after the last call.");
    });
}
于 2021-12-22T19:22:01.537 回答
-1

我知道我在这个聚会上迟到了几十万分钟,但我想我会加我的 2 美分。我很惊讶没有人提出这个建议,所以我假设有一些我不知道的东西可能会使它不太理想,所以如果它被击落,也许我会学到一些新东西。我经常使用使用System.Threading.Timer'Change()方法的解决方案。

using System.Threading;

Timer delayedActionTimer;

public MyClass()
{
    // Setup our timer
    delayedActionTimer = new Timer(saveOrWhatever, // The method to call when triggered
                                   null, // State object (Not required)
                                   Timeout.Infinite, // Start disabled
                                   Timeout.Infinite); // Don't repeat the trigger
}

// A change was made that we want to save but not until a
// reasonable amount of time between changes has gone by
// so that we're not saving on every keystroke/trigger event.
public void TextChanged()
{
    delayedActionTimer.Change(3000, // Trigger this timers function in 3 seconds,
                                    // overwriting any existing countdown
                              Timeout.Infinite); // Don't repeat this trigger; Only fire once
}

// Timer requires the method take an Object which we've set to null since we don't
// need it for this example
private void saveOrWhatever(Object obj) 
{
    /*Do the thing*/
}
于 2019-01-21T20:18:41.407 回答