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 секунд, прежде чем в конечном итоге будут доступны для сбора мусора.