Сбой события С#
Я слушаю сообщение об аппаратном событии, но мне нужно его отбросить, чтобы избежать слишком большого количества запросов.
Это аппаратное событие, которое отправляет состояние машины, и я должен хранить его в базе данных для статистических целей, и иногда случается, что его статус очень часто изменяется (мерцает?). В этом случае я хотел бы сохранить только "стабильный" статус, и я хочу реализовать его, просто дожидаясь 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, я должен отклонить первый запрос и начать ждать времени debounce, чтобы завершить второе событие.
Каков наилучший способ управления? Просто одноразовый таймер?
Чтобы объяснить функцию "debounce", см. эту реализацию javascript для ключевых событий:
http://benalman.com/code/projects/jquery-throttle-debounce/examples/debounce/
Ответы
Ответ 1
Это не тривиальный запрос на код с нуля, поскольку существует несколько нюансов. Аналогичным сценарием является мониторинг FileSystemWatcher и ожидание того, чтобы все стало тихо, после большой копии, прежде чем пытаться открыть измененные файлы.
Реактивные расширения в .NET 4.5 были созданы для обработки именно этих сценариев. Вы можете легко использовать их для обеспечения таких функций такими методами, как Throttle, Buffer, Window или Пример. Вы публикуете события в Subject, применяете к нему одну из функций оконной обработки, например, чтобы получать уведомление, только если не было активности для X секунд или Y, а затем подписаться на уведомление.
Subject<MyEventData> _mySubject=new Subject<MyEventData>();
....
var eventSequenc=mySubject.Throttle(TimeSpan.FromSeconds(1))
.Subscribe(events=>MySubscriptionMethod(events));
Throttle возвращает последнее событие в скользящем окне, только если в окне не было других событий. Любое событие сбрасывает окно.
Вы можете найти очень хороший обзор смещенных по времени функций здесь
Когда ваш код получает событие, вам нужно только отправить его в тему с помощью OnNext:
_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));
Вы также можете создавать наблюдаемые из задач, объединять последовательности событий с операторами LINQ, чтобы запросить, например: пары различных аппаратных событий с Zip, использовать другой источник событий для привязки дросселя/буфера и т.д., добавить задержки и многое другое.
Reactive Extensions доступен как пакет NuGet, поэтому очень легко добавить их в свой проект.
Книга Стивена Клири "Concurrency в С# Cookbook "- это очень хороший ресурс для реактивных расширений, среди прочего, и объясняет, как вы можете использовать его и как он подходит для остальных параллельных API в .NET, таких как задачи, события и т.д.
Введение в Rx - отличная серия статей (где я скопировал образцы), с несколькими примерами.
UPDATE
Используя ваш конкретный пример, вы можете сделать что-то вроде:
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 выставлял и располагал свой собственный наблюдаемый.
Ответ 2
Я использовал это, чтобы отбросить события с некоторым успехом:
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, но его легко понять и использовать.
Ответ 3
Недавно я делал некоторое обслуживание в приложении, которое предназначалось для более старой версии .NET framework (v3.5).
Я не мог использовать Reactive Extensions или Task Parallel Library, но мне нужен был хороший, чистый, последовательный способ debouncing событий. Вот что я придумал:
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();
}
}
}
}
}
Вот пример использования его в виде окна, в котором есть текстовое поле поиска:
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
}
}
}
Ответ 4
Ответ Panagiotis, безусловно, правильный, однако я хотел привести более простой пример, поскольку мне потребовалось некоторое время, чтобы разобраться, как заставить его работать. Мой сценарий заключается в том, что пользователь вводит в поле поиска, и по мере того как типы пользователей мы хотим сделать api-вызовы для возврата предложений поиска, поэтому мы хотим отменить вызовы api, чтобы они не делали их каждый раз, когда они набирают символ.
Я использую Xamarin.Android, однако это должно применяться к любому сценарию С#...
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 секунда) он будет вызывать подписанное событие (предложенияAdapter.Get в нашем случае.)
Наконец, когда экран закрыт, убедитесь, что вы выбрали подписку!
Ответ 5
У меня возникли проблемы с этим. Я пробовал каждый из ответов здесь, и поскольку я нахожусь в универсальном приложении 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 на шаге.
Ответ 6
Просто помните последний хит:
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;
}
Ответ 7
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 мс - любые предыдущие ожидающие события отбрасываются.
Также существует метод 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;
}
Ответ 8
Я придумал это в своем определении класса.
Я хотел немедленно запустить свое действие, если в течение периода времени не было никаких действий (в примере 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 секунд, прежде чем они станут доступны для сбора мусора.
Ответ 9
Я знаю, что опоздал на эту вечеринку на пару сотен тысяч минут, но я решил добавить свои 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*/
}