我正在收听硬件事件消息,但我需要对其进行辩护以避免过多的查询。
这是一个发送机器状态的硬件事件,我必须将其存储在数据库中以用于统计目的,并且有时会发生其状态经常变化(闪烁?)。在这种情况下,我只想存储一个稳定的"状态,我想通过在将状态存储到数据库之前等待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"函数请看关键事件的这个javascript实现: http://benalman.com/code/projects/jquery-throttle-debounce/examples/debounce/
答案 0 :(得分:33)
这不是一个从头开始编码的简单请求,因为有几个细微差别。类似的情况是在尝试打开修改后的文件之前监视FileSystemWatcher并在大型副本之后等待安静下来。
创建.NET 4.5中的Reactive Extensions以完全处理这些场景。您可以轻松地使用它们来提供此类功能,例如Throttle,Buffer,Window或Sample。您将事件发布到Subject,将其中一个窗口函数应用于它,例如,仅在X秒或Y事件没有活动时才收到通知,然后订阅通知。
Subject<MyEventData> _mySubject=new Subject<MyEventData>();
....
var eventSequenc=mySubject.Throttle(TimeSpan.FromSeconds(1))
.Subscribe(events=>MySubscriptionMethod(events));
仅当窗口中没有其他事件时,Throttle才会返回滑动窗口中的最后一个事件。任何事件都会重置窗口。
您可以很好地了解时移函数here
当您的代码收到该事件时,您只需将其发布到具有OnNext的主题:
_mySubject.OnNext(MyEventData);
如果您的硬件事件表现为典型的.NET事件,您可以绕过使用Observable.FromEventPattern的主题和手动发布,如图here所示:
var mySequence = Observable.FromEventPattern<MyEventData>(
h => _myDevice.MyEvent += h,
h => _myDevice.MyEvent -= h);
_mySequence.Throttle(TimeSpan.FromSeconds(1))
.Subscribe(events=>MySubscriptionMethod(events));
您还可以从Tasks创建observable,将事件序列与LINQ运算符组合以请求例如:使用Zip的不同硬件事件对,使用另一个事件源来绑定Throttle / Buffer等,添加延迟等等。
Reactive Extensions以NuGet package形式提供,因此将它们添加到您的项目中非常容易。
Stephen Cleary的书“Concurrency in C# Cookbook”是关于Reactive Extensions的非常好资源,并解释了如何使用它以及它如何与其余的并发API相匹配在.NET中,如任务,事件等。
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,以便每个MachineClass公开并处理它自己的observable。
答案 1 :(得分:32)
我已经用它来取消事件并取得了一些成功:
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中的那样强大,但它易于理解和使用。
答案 2 :(得分:7)
最近,我对一个针对旧版.NET Framework(v3.5)的应用程序进行了一些维护。
我无法使用Reactive Extensions或任务并行库,但我需要一种漂亮,干净,一致的方法来去除事件。这就是我想出的:
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
}
}
}
答案 3 :(得分:5)
我正在使用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秒)之后,它将调用订阅的事件(在我们的情况下,adviceAdapter.Get。)
最后,当屏幕关闭时,请务必处理订阅!
答案 4 :(得分:4)
我遇到了这个问题。我在这里尝试了每个答案,因为我在一个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
}
}
});
}
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。
答案 5 :(得分:2)
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();
});
}
键事件现在仅在键盘空闲200ms后处理 - 任何先前的待处理事件都将被丢弃。
还有一个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;
}
答案 6 :(得分:1)
只需记住最新的&#39;点击:
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
...
}
如果在最后一次事件之后有足够的时间,这将允许第二次事件。
请注意:在一系列快速事件中处理 last 事件是不可能的(以简单的方式),因为你永远不知道哪一个是 last ...
...除非你准备好处理很久以前爆发的最后一次事件。然后你必须记住最后一个事件并在下一个事件足够缓慢时记录它:
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;
}
答案 7 :(得分:1)
这颗小宝石是由Mike Wards极富创意的extension尝试启发而来的。但是,此文件可以很好地清除。
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.");
}
答案 8 :(得分:1)
我需要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
});
}
}
答案 9 :(得分: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秒钟,然后才能最终用于垃圾收集。
答案 10 :(得分:0)
我知道我晚了十万分钟参加这场聚会,但我想我要加2美分。我很惊讶没有人提出这个建议,所以我假设有一些我不知道的事情可能会使它不理想,所以如果被击落,也许我会学到一些新东西。
我经常使用使用System.Threading.Timer
的{{1}}方法的解决方案。
Change()
答案 11 :(得分:0)
这受Nieminen基于Task.Delay的Debouncer class的启发。简化后的一些较小的更正,应该更好地进行清理。
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);
答案 12 :(得分: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是事件处理程序。
答案 13 :(得分:0)
我需要类似的内容,但是在Web应用程序中,因此我无法将df %>%
group_by(id_choice) %>%
mutate(across(starts_with("V"), choice_generator2))
存储在变量中,它将在http请求之间丢失。
基于其他答案和@Collie的想法,我创建了一个类,该类看起来具有唯一的字符串键以进行调节。
df %>%
group_by(id_choice) %>%
mutate(across(starts_with("V"), choice_generator2, .names = "choice_{.col}"))
# A tibble: 9 x 7
# Groups: id_choice [3]
# id_choice V1 V2 V3 choice_V1 choice_V2 choice_V3
# <int> <dbl> <dbl> <dbl> <int> <int> <int>
# 1 1 -2.16 -0.708 -0.708 1 1 0
# 2 1 -8.00 -2.47 -2.47 0 0 1
# 3 1 -2.75 -0.930 -0.930 0 0 0
# 4 2 -10.4 -5.02 -3.34 0 0 0
# 5 2 -1.24 -0.602 -0.420 1 0 0
# 6 2 -0.378 -0.187 -0.141 0 1 1
# 7 3 -5.41 -2.06 -2.06 0 0 0
# 8 3 -1.92 -0.706 -0.706 1 0 1
# 9 3 -3.43 -1.05 -1.05 0 1 0
用法:
Action
作为附带的奖励,因为它基于字符串键,所以可以使用 inline lambda的
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);
}
}
答案 14 :(得分:0)
我编写了一个不运行 async-in-sync 的异步去抖动器。
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();
}
我使用它来消除文件更改报告的更改,请参阅完整示例 here。
答案 15 :(得分: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);
}
}
};
}