C# событие debounce
Я слушаю сообщение об аппаратном событии, но мне нужно его разоблачить, чтобы избежать слишком много запросов.
это аппаратное событие, которое отправляет статус машины, и я должен хранить его в базе данных для статистических целей, и иногда случается, что его статус меняется очень часто (мерцание?). В этом случае я хотел бы сохранить только "стабильный" статус, и я хочу реализовать его, просто дождавшись 1-2s перед сохранением статуса в базе данных.
Это мой код:
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, я должен отклонить первый запрос и начать ждать времени debounce для завершения второго события.
каков наилучший выбор для управления им? Просто одноразовый таймер?
чтобы объяснить функцию "debounce", см. эту реализацию javascript для ключа события: http://benalman.com/code/projects/jquery-throttle-debounce/examples/debounce/
8 ответов
это не тривиальный запрос на код с нуля, поскольку есть несколько нюансов. Аналогичный сценарий отслеживает FileSystemWatcher и ждет, пока все успокоится после большой копии, прежде чем пытаться открыть измененные файлы.
реактивные расширения в .NET 4.5 были созданы для обработки именно этих сценариев. Вы можете легко использовать их, чтобы обеспечить такую функциональность такими методами, как дроссель, буфер, окно или пример. Вы публикуете события в теме, примените к нему одну из оконных функций, например, чтобы получить уведомление, только если не было активности в течение X секунд или событий Y, а затем Подписаться на уведомление.
Subject<MyEventData> _mySubject=new Subject<MyEventData>();
....
var eventSequenc=mySubject.Throttle(TimeSpan.FromSeconds(1))
.Subscribe(events=>MySubscriptionMethod(events));
дроссель возвращает последнее событие в скользящем окне, только если в окне не было других событий. Любое событие сбрасывает окно.
вы можете найти очень хороший обзор функций, сдвинутых во времени здесь
когда ваш код получает событие, вам нужно только опубликовать его в теме с помощью OnNext:
_mySubject.OnNext(MyEventData);
если ваше аппаратное событие отображается как типичное событие .NET, вы можете обойти тему и ручную публикацию с помощью наблюдаема.FromEventPattern, как показано здесь:
var mySequence = Observable.FromEventPattern<MyEventData>(
h => _myDevice.MyEvent += h,
h => _myDevice.MyEvent -= h);
_mySequence.Throttle(TimeSpan.FromSeconds(1))
.Subscribe(events=>MySubscriptionMethod(events));
вы также можете создавать наблюдаемые из задач, комбинировать последовательности событий с операторами LINQ для запроса, например: пары различного оборудования события с Zip, используйте другой источник событий для привязки дроссельной заслонки / буфера и т. д., Добавьте задержки и многое другое.
реактивные расширения доступны как пакета NuGet, поэтому их очень легко добавить в ваш проект.
книга Стивена Клири"параллелизм в C# Cookbook" - это очень хороший ресурс для реактивных расширений, среди прочего, и объясняет, как вы можете его использовать и как он соответствует остальным параллельным API в .NET как задачи, события и т. д.
введение в 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;
}
это может быть значительно улучшено, конечно, - как наблюдаемый, так и подписка должны быть удалены в какой-то момент. Этот код предполагает, что вы управляете только одним устройством. Если у вас есть много устройств, вы можете создать наблюдаемое внутри класса, чтобы каждый 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, но это легко понять и использовать.
ответ Панагиотиса, безусловно, правильный, однако я хотел дать более простой пример, так как мне потребовалось некоторое время, чтобы разобраться, как заставить его работать. Мой сценарий заключается в том, что пользователь вводит в поле поиска, и как типы пользователей мы хотим сделать вызовы 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.Залезай в наш случай.)
наконец, когда экран закрыт, убедитесь, что избавиться от подписки!
недавно я выполнял некоторое обслуживание приложения, которое было нацелено на более старую версию .NET framework (v3.5).
Я не мог использовать реактивные расширения или параллельную библиотеку задач, но мне нужен был хороший, чистый, последовательный способ разбора событий. Вот что я придумал:--3-->
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
}
}
}
просто помните последний " хит:
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;
}
RX, вероятно, самый простой выбор, особенно если вы уже используете его в своем приложении. Но если нет, добавление может быть немного излишним.
для приложений на основе пользовательского интерфейса (например, 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 мс - любые предыдущие отложенные события отбрасываются.
есть также метод дроссельной заслонки, который всегда запускает события через заданный интервал:
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;
}
я столкнулся с этим проблемы. Я попробовал каждый из ответов здесь, и поскольку я нахожусь в универсальном приложении 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;
public Debouncer(int millisecondsToWait = 300)
{
this.MillisecondsToWait = millisecondsToWait;
}
public void Debouce(Action func)
{
CancelAllStepperTokens(); // Cancel all api requests;
var newTokenSrc = new CancellationTokenSource();
StepperCancelTokens.Add(newTokenSrc);
Task.Delay(MillisecondsToWait, newTokenSrc.Token).ContinueWith(task => // Create new request
{
if (!newTokenSrc.IsCancellationRequested) // if it hasn't been cancelled
{
func(); // run
CancelAllStepperTokens(); // Cancel any that remain (there shouldn't be any)
StepperCancelTokens = new List<CancellationTokenSource>(); // set to new list
}
});
}
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 на шаге.
Я придумал это в своем определении класса.
Я хотел запустить свое действие немедленно, если в течение периода времени не было никаких действий (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&a4&ea5e&a6e&a7e&a8e&a9e&a0e&a
EVENT: A B C D E F
OBSERVED: A B E F
обратите внимание, что не предпринимается никаких попыток отменить задачу раньше, поэтому действия могут накапливаться в течение 3 секунд, прежде чем в конечном итоге будут доступны для сбора мусора.