C# 事件去抖动
C# event debounce
我正在收听硬件事件消息,但我需要对其进行去抖动以避免查询过多。
这是一个发送机器状态的硬件事件,我必须将它存储在数据库中用于统计目的,有时它的状态会经常变化(闪烁?)。在这种情况下,我只想存储一个 "stable" 状态,我想通过在将状态存储到数据库之前等待 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");
}
我称此行为为"debounce":等待几次以真正完成它的工作:如果在去抖时间内再次触发相同的事件,我必须取消第一个请求并开始等待去抖是时候完成第二项活动了。
管理它的最佳选择是什么?只是一个一次性定时器?
要解释 "debounce" 函数,请参阅此 javascript 键事件实现:
http://benalman.com/code/projects/jquery-throttle-debounce/examples/debounce/
只需记住最新的'命中:
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;
}
这不是一个从头开始编写代码的微不足道的请求,因为其中存在一些细微差别。一个类似的场景是监视 FileSystemWatcher 并等待在大复制之后安静下来,然后再尝试打开修改后的文件。
.NET 4.5 中的 Reactive Extensions 正是为处理这些场景而创建的。您可以轻松地使用它们通过 Throttle, Buffer, Window or Sample. You post the events to a Subject 等方法提供此类功能,对其应用 windowing 函数之一,例如仅在 X 没有 activity 时才收到通知秒或 Y 事件,然后订阅通知。
Subject<MyEventData> _mySubject=new Subject<MyEventData>();
....
var eventSequenc=mySubject.Throttle(TimeSpan.FromSeconds(1))
.Subscribe(events=>MySubscriptionMethod(events));
限制 returns 滑动 window 中的最后一个事件,前提是 window 中没有其他事件。任何事件都会重置 window.
你可以找到时移函数的一个很好的概述here
当您的代码接收到事件时,您只需要使用 OnNext post 将它发送到 Subject:
_mySubject.OnNext(MyEventData);
如果您的硬件事件表现为典型的 .NET 事件,您可以绕过主题和手动 posting Observable.FromEventPattern, as shown here:
var mySequence = Observable.FromEventPattern<MyEventData>(
h => _myDevice.MyEvent += h,
h => _myDevice.MyEvent -= h);
_mySequence.Throttle(TimeSpan.FromSeconds(1))
.Subscribe(events=>MySubscriptionMethod(events));
您还可以从 Tasks 创建可观察对象,将事件序列与 LINQ 运算符结合起来以请求例如:使用 Zip 成对的不同硬件事件,使用另一个事件源来绑定 Throttle/Buffer 等,添加延迟等等.
Reactive Extensions 以 NuGet package 的形式提供,因此将它们添加到您的项目中非常容易。
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 和 subscription 都需要在某个时候被处理掉。此代码假定您只控制一个设备。如果您有很多设备,您可以在 class 中创建可观察对象,以便每个 MachineClass 公开并配置自己的可观察对象。
我用它来消除事件并取得了一些成功:
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 不会 canceled/disposed 直到下一次调用
- 如@collie 所述,任务已处理完毕,因此无需在任务上调用
Dispose
我以前没有使用过取消令牌,可能没有正确使用它们。
Panagiotis 的回答当然是正确的,但是我想举一个更简单的例子,因为我花了一些时间来整理如何让它工作。我的场景是,用户在搜索框中键入内容,当用户键入内容时,我们希望对 return 搜索建议进行 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 ();
}
当您第一次初始化屏幕时/class,您创建事件来监听用户输入 (SearchTextChanged),然后还设置了一个限制订阅,它绑定到 "typingSubject".
接下来,在您的 SearchTextChanged 事件中,您可以调用 typingSubject.OnNext 并传入搜索框的文本。在去抖周期(1 秒)之后,它将调用订阅的事件(在我们的例子中为 suggestionsAdapter.Get。)
最后,当屏幕关闭时,请务必取消订阅!
RX 可能是最简单的选择,尤其是当您已经在您的应用程序中使用它时。但如果没有,添加它可能有点矫枉过正。
对于基于 UI 的应用程序(如 WPF),我使用以下使用 DispatcherTimer 的 class:
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;
}
最近我正在对一个针对旧版本 .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
}
}
}
我 运行 遇到了这个问题。我在这里尝试了每个答案,因为我在 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。
我在 class 定义中想到了这个。
如果在一段时间内(示例中为 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 秒,然后最终可用于垃圾回收。
我知道我迟到了几十万分钟,但我想我应该加 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*/
}
这个小小的 gem 灵感来自 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);
};
}
注意:在这种情况下不需要处理任务。证据见 here。
用法
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.");
}
这是受 Nieminen 基于 Task.Delay 的 的启发。简化,一些小的更正,并且应该更好地清理。
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);
我需要 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) { }
}
}
搜索功能调用示例[=12=]
<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
});
}
}
弄清楚如何使用 System.Reactive NuGet 包对 TextBox 进行适当的去抖动。
在class级别,我们有我们的字段
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是事件处理器。
我需要这样的东西,但是在网络应用程序中,所以我不能将 Action
存储在变量中,它将在 http 请求之间丢失。
根据其他答案和@Collie 的想法,我创建了一个 class,它看起来 一个唯一的字符串键 以进行节流。
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);
作为附带奖励,因为它基于字符串键,您可以使用 inline lambda's
Debouncer.Debounce("Some-Unique-ID", () =>
{
//do some work here
}, 5);
我写了一个不运行异步同步的异步去抖器。
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();
}
我用它来消除文件更改报告的更改,请参阅完整示例 。
我受到 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);
}
}
};
}
创建此 class 以解决等待呼叫的问题:
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);
我根据@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.");
});
}
另一个实现
public static class Debounce
{
public static Action Action(Action action, TimeSpan time)
{
var timer = new Timer(_ => action(), null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
return () => timer.Change(time, Timeout.InfiniteTimeSpan);
}
}
我正在收听硬件事件消息,但我需要对其进行去抖动以避免查询过多。
这是一个发送机器状态的硬件事件,我必须将它存储在数据库中用于统计目的,有时它的状态会经常变化(闪烁?)。在这种情况下,我只想存储一个 "stable" 状态,我想通过在将状态存储到数据库之前等待 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");
}
我称此行为为"debounce":等待几次以真正完成它的工作:如果在去抖时间内再次触发相同的事件,我必须取消第一个请求并开始等待去抖是时候完成第二项活动了。
管理它的最佳选择是什么?只是一个一次性定时器?
要解释 "debounce" 函数,请参阅此 javascript 键事件实现: http://benalman.com/code/projects/jquery-throttle-debounce/examples/debounce/
只需记住最新的'命中:
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;
}
这不是一个从头开始编写代码的微不足道的请求,因为其中存在一些细微差别。一个类似的场景是监视 FileSystemWatcher 并等待在大复制之后安静下来,然后再尝试打开修改后的文件。
.NET 4.5 中的 Reactive Extensions 正是为处理这些场景而创建的。您可以轻松地使用它们通过 Throttle, Buffer, Window or Sample. You post the events to a Subject 等方法提供此类功能,对其应用 windowing 函数之一,例如仅在 X 没有 activity 时才收到通知秒或 Y 事件,然后订阅通知。
Subject<MyEventData> _mySubject=new Subject<MyEventData>();
....
var eventSequenc=mySubject.Throttle(TimeSpan.FromSeconds(1))
.Subscribe(events=>MySubscriptionMethod(events));
限制 returns 滑动 window 中的最后一个事件,前提是 window 中没有其他事件。任何事件都会重置 window.
你可以找到时移函数的一个很好的概述here
当您的代码接收到事件时,您只需要使用 OnNext post 将它发送到 Subject:
_mySubject.OnNext(MyEventData);
如果您的硬件事件表现为典型的 .NET 事件,您可以绕过主题和手动 posting Observable.FromEventPattern, as shown here:
var mySequence = Observable.FromEventPattern<MyEventData>(
h => _myDevice.MyEvent += h,
h => _myDevice.MyEvent -= h);
_mySequence.Throttle(TimeSpan.FromSeconds(1))
.Subscribe(events=>MySubscriptionMethod(events));
您还可以从 Tasks 创建可观察对象,将事件序列与 LINQ 运算符结合起来以请求例如:使用 Zip 成对的不同硬件事件,使用另一个事件源来绑定 Throttle/Buffer 等,添加延迟等等.
Reactive Extensions 以 NuGet package 的形式提供,因此将它们添加到您的项目中非常容易。
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 和 subscription 都需要在某个时候被处理掉。此代码假定您只控制一个设备。如果您有很多设备,您可以在 class 中创建可观察对象,以便每个 MachineClass 公开并配置自己的可观察对象。
我用它来消除事件并取得了一些成功:
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 不会 canceled/disposed 直到下一次调用
- 如@collie 所述,任务已处理完毕,因此无需在任务上调用
Dispose
我以前没有使用过取消令牌,可能没有正确使用它们。
Panagiotis 的回答当然是正确的,但是我想举一个更简单的例子,因为我花了一些时间来整理如何让它工作。我的场景是,用户在搜索框中键入内容,当用户键入内容时,我们希望对 return 搜索建议进行 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 ();
}
当您第一次初始化屏幕时/class,您创建事件来监听用户输入 (SearchTextChanged),然后还设置了一个限制订阅,它绑定到 "typingSubject".
接下来,在您的 SearchTextChanged 事件中,您可以调用 typingSubject.OnNext 并传入搜索框的文本。在去抖周期(1 秒)之后,它将调用订阅的事件(在我们的例子中为 suggestionsAdapter.Get。)
最后,当屏幕关闭时,请务必取消订阅!
RX 可能是最简单的选择,尤其是当您已经在您的应用程序中使用它时。但如果没有,添加它可能有点矫枉过正。
对于基于 UI 的应用程序(如 WPF),我使用以下使用 DispatcherTimer 的 class:
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;
}
最近我正在对一个针对旧版本 .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
}
}
}
我 运行 遇到了这个问题。我在这里尝试了每个答案,因为我在 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。
我在 class 定义中想到了这个。
如果在一段时间内(示例中为 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 秒,然后最终可用于垃圾回收。
我知道我迟到了几十万分钟,但我想我应该加 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*/
}
这个小小的 gem 灵感来自 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);
};
}
注意:在这种情况下不需要处理任务。证据见 here。
用法
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.");
}
这是受 Nieminen 基于 Task.Delay 的
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);
我需要 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) { }
}
}
搜索功能调用示例[=12=]
<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
});
}
}
弄清楚如何使用 System.Reactive NuGet 包对 TextBox 进行适当的去抖动。
在class级别,我们有我们的字段
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是事件处理器。
我需要这样的东西,但是在网络应用程序中,所以我不能将 Action
存储在变量中,它将在 http 请求之间丢失。
根据其他答案和@Collie 的想法,我创建了一个 class,它看起来 一个唯一的字符串键 以进行节流。
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);
作为附带奖励,因为它基于字符串键,您可以使用 inline lambda's
Debouncer.Debounce("Some-Unique-ID", () =>
{
//do some work here
}, 5);
我写了一个不运行异步同步的异步去抖器。
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();
}
我用它来消除文件更改报告的更改,请参阅完整示例
我受到 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);
}
}
};
}
创建此 class 以解决等待呼叫的问题:
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);
我根据@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.");
});
}
另一个实现
public static class Debounce
{
public static Action Action(Action action, TimeSpan time)
{
var timer = new Timer(_ => action(), null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
return () => timer.Change(time, Timeout.InfiniteTimeSpan);
}
}