c# events debouncing

Evento C#debounce



events debouncing (8)

Estoy escuchando un mensaje de evento de hardware, pero necesito rebotar para evitar demasiadas consultas.

Este es un evento de hardware que envía el estado de la máquina y tengo que almacenarlo en una base de datos con fines estadísticos, y a veces sucede que su estado cambia muy a menudo (¿parpadea? En este caso, me gustaría almacenar solo un estado "estable" y quiero implementarlo simplemente esperando 1-2s antes de almacenar el estado en la base de datos.

Este es mi código:

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"); }

Llamo a este comportamiento "rebotar": espere un par de veces para hacer realmente su trabajo: si el mismo evento se dispara nuevamente durante el tiempo de rebote, debo descartar la primera solicitud y comenzar a esperar el tiempo de rebote para completar el segundo evento.

¿Cuál es la mejor opción para manejarlo? ¿Simplemente un temporizador de un disparo?

Para explicar la función "rebotar", consulte esta implementación de javascript para eventos clave: http://benalman.com/code/projects/jquery-throttle-debounce/examples/debounce/


Esta no es una solicitud trivial para codificar desde cero, ya que hay varios matices. Un escenario similar es monitorear un FileSystemWatcher y esperar que las cosas se calmen después de una copia grande, antes de intentar abrir los archivos modificados.

Las extensiones reactivas en .NET 4.5 fueron creadas para manejar exactamente estos escenarios. Puede usarlos fácilmente para proporcionar dicha funcionalidad con métodos como Throttle , Buffer , Window o Sample . Usted publica los eventos en un Subject , le aplica una de las funciones de ventanas, por ejemplo, para obtener una notificación solo si no hubo actividad durante X segundos o eventos Y, luego suscríbase a la notificación.

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

Throttle devuelve el último evento en una ventana deslizante, solo si no hubo otros eventos en la ventana. Cualquier evento restablece la ventana.

Puede encontrar una muy buena visión general de las funciones desplazadas en el tiempo here

Cuando su código recibe el evento, solo necesita publicarlo en el Asunto con OnNext:

_mySubject.OnNext(MyEventData);

Si su evento de hardware aparece como un evento típico de .NET, puede omitir el Asunto y la publicación manual con Observable.FromEventPattern , como se muestra here :

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

También puede crear observables desde Tareas, combinar secuencias de eventos con operadores LINQ para solicitar, por ejemplo: pares de diferentes eventos de hardware con Zip, usar otra fuente de eventos para enlazar Throttle / Buffer, etc., agregar retrasos y mucho más.

Las extensiones reactivas están disponibles como un paquete NuGet , por lo que es muy fácil agregarlas a su proyecto.

El libro de Stephen Cleary " Concurrency in C # Cookbook " es un muy buen recurso en Reactive Extensions entre otras cosas, y explica cómo puede usarlo y cómo encaja con el resto de las API concurrentes en .NET como Tareas, Eventos, etc.

Introduction to Rx es una excelente serie de artículos (de ahí copié las muestras), con varios ejemplos.

ACTUALIZAR

Usando tu ejemplo específico, podrías hacer algo como:

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; }

Por supuesto, esto se puede mejorar en gran medida: tanto lo observable como la suscripción deben eliminarse en algún momento. Este código asume que solo controlas un solo dispositivo. Si tiene muchos dispositivos, puede crear lo observable dentro de la clase para que cada MachineClass exponga y disponga de su propio observable.


He usado esto para rebotar eventos con cierto éxito:

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(); }); }; }

Uso

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); }

Puede que no sea tan robusto como el RX, pero es fácil de entender y usar.


La respuesta de Panagiotis es ciertamente correcta, sin embargo, quería dar un ejemplo más simple, ya que me tomó un tiempo averiguar cómo hacerlo funcionar. Mi situación es que un usuario escribe en un cuadro de búsqueda y, a medida que el usuario escribe, queremos hacer llamadas a la API para devolver las sugerencias de búsqueda, por lo que queremos eliminar las llamadas de la API para que no hagan una cada vez que escriben un carácter.

Estoy usando Xamarin.Android, sin embargo, esto debería aplicarse a cualquier escenario de 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 (); }

Cuando inicializa la pantalla / clase por primera vez, crea su evento para escuchar al usuario que escribe (SearchTextChanged) y, a continuación, también configura una suscripción de limitación, que está vinculada a "typingSubject".

A continuación, en su evento SearchTextChanged, puede llamar a typingSubject.OnNext y pasar el texto del cuadro de búsqueda. Después del período de rebote (1 segundo), llamará al evento suscrito (Suggestions.Adapt.Get en nuestro caso).

Por último, cuando se cierre la pantalla, ¡asegúrese de deshacerse de la suscripción!


Me encontré con problemas con esto. Intenté cada una de las respuestas aquí, y como estoy en una aplicación universal de Xamarin, parece que me faltan algunas cosas que se requieren en cada una de estas respuestas, y no quería agregar más paquetes o bibliotecas. Mi solución funciona exactamente como lo esperaba, y no he tenido ningún problema con ella. Espero que ayude a alguien.

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(); } } } } }

Se llama así ...

private Debouncer StepperDeboucer = new Debouncer(1000); // one second StepperDeboucer.Debouce(() => { WhateverMethod(args) });

No recomendaría esto para cualquier cosa en la que la máquina pueda enviar cientos de solicitudes por segundo, pero para la opinión del usuario, funciona de manera excelente. Lo estoy usando en un paso a paso en una aplicación de Android / IOS que llama a una API en el paso.


RX es probablemente la opción más fácil, especialmente si ya lo está utilizando en su aplicación. Pero si no, agregarlo podría ser un poco excesivo.

Para aplicaciones basadas en IU (como WPF), uso la siguiente clase que usa 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(); } }

Para usarlo:

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(); }); }

Los eventos clave ahora solo se procesan después de que el teclado esté inactivo durante 200 ms: se descartan los eventos pendientes anteriores.

También hay un método Throttle que siempre dispara eventos después de un intervalo dado:

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; }


Recientemente estuve realizando algunas tareas de mantenimiento en una aplicación que tenía como objetivo una versión anterior de .NET framework (v3.5).

No pude usar las Extensiones reactivas ni la Biblioteca paralela de tareas, pero necesitaba una forma agradable, limpia y coherente de anunciar eventos. Esto es lo que se me ocurrió:

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(); } } } } }

Este es un ejemplo de su uso en un formulario de Windows que tiene un cuadro de texto de búsqueda:

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 } } }


Se me ocurrió esto en mi definición de clase.

Quería ejecutar mi acción inmediatamente si no ha habido ninguna acción durante el período de tiempo (3 segundos en el ejemplo).

Si algo sucedió en los últimos tres segundos, quiero enviar lo último que sucedió dentro de ese tiempo.

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(); } }

Entonces, si tengo que subdividir mi reloj en cada cuarto de segundo

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

Tenga en cuenta que no se intenta cancelar la tarea antes, por lo que es posible que las acciones se acumulen durante 3 segundos antes de que estén disponibles para la recolección de basura.


Simplemente recuerda el último golpe:

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 ... }

Esto permitirá un segundo evento si hubo suficiente tiempo después del último evento.

Tenga en cuenta que no es posible (de manera simple) manejar el último evento en una serie de eventos rápidos, porque nunca se sabe cuál es el último ...

... a menos que esté preparado para manejar el último evento de una explosión que fue hace mucho tiempo . Luego tienes que recordar el último evento y registrarlo si el siguiente evento es lo suficientemente lento:

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; }