web-dev-qa-db-ja.com

C#イベントのデバウンス

私はハードウェアイベントメッセージを聞いていますが、あまりにも多くのクエリを避けるために、それをデバウンスする必要があります。

これは、マシンステータスを送信するハードウェアイベントであり、統計のためにデータベースに保存する必要があり、時々ステータスが頻繁に変化する(ちらつく?)この場合、「安定した」ステータスのみを保存し、データベースにステータスを保存する前に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");
}

私はこの動作を「デバウンス」と呼びます:実際にその仕事をするために数回待ちます:デバウンス時間中に同じイベントが再び発生した場合、最初のリクエストを却下し、デバウンス時間を待って2番目のイベントを完了する必要があります。

それを管理するための最良の選択は何ですか?単発タイマーですか?

「デバウンス」機能を説明するには、主要なイベントについて次のJavaScript実装を参照してください。 http://benalman.com/code/projects/jquery-throttle-debounce/examples/debounce/

30
Tobia

いくつかのニュアンスがあるため、これはゼロからコードを作成するという些細な要求ではありません。同様のシナリオは、FileSystemWatcherを監視し、大きなコピーの後、変更されたファイルを開こうとする前に、物事が静かになるのを待っています。

.NET 4.5のリアクティブエクステンションは、これらのシナリオを正確に処理するために作成されました。これらを使用して、 ThrottleBufferWindow 、または Sample などのメソッドでこのような機能を簡単に提供できます。イベントを Subject に投稿し、ウィンドウ関数の1つを適用します。たとえば、X秒またはYイベントのアクティビティがない場合にのみ通知を取得し、通知をサブスクライブします。

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

Throttleは、ウィンドウに他のイベントがなかった場合にのみ、スライディングウィンドウに最後のイベントを返します。すべてのイベントはウィンドウをリセットします。

タイムシフト機能の非常に優れた概要を見つけることができます here

コードがイベントを受け取ったら、OnNextでSubjectに投稿するだけです。

_mySubject.OnNext(MyEventData);

ハードウェアイベントが典型的な.NETイベントとして表面化する場合、 here のように 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パッケージ として利用できるため、プロジェクトに簡単に追加できます。

Stephen Clearyの本「 C#Cookbookでの同時実行 」は、Reactive Extensionsのvery優れたリソースであり、その使用方法と使用方法について説明しています。タスク、イベントなどの.NETの残りの並行APIに適合します。

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が独自のオブザーバブルを公開および破棄できるようにすることができます。

35

イベントをデバウンスするためにこれを使用して、いくつかの成功を収めました。

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の機能ほど堅牢ではないかもしれませんが、理解して使用するのは簡単です。

35
Mike Ward

最近、古いバージョンの.NETフレームワーク(v3.5)を対象としたアプリケーションのメンテナンスを行っていました。

Reactive ExtensionsもTask Parallel Libraryも使用できませんでしたが、イベントをデバウンスするためのすてきできれいな、一貫した方法が必要でした。ここに私が思いついたものがあります:

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
        }
    }
}
8
Ronnie Overby

これで問題に遭遇しました。ここで各回答を試してみましたが、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) });

マシンが1秒間に何百ものリクエストを送信する可能性がある場合は、これをお勧めしませんが、ユーザー入力については非常にうまく機能します。私は、ステップでAPIを呼び出すAndroid/IOSアプリのステッパーで使用しています。

5
Nieminen

パナギオティスの答えは確かに正しいが、より簡単な例を挙げたいと思った。私のシナリオは、ユーザーが検索ボックスに入力し、ユーザーが入力するときに検索候補を返すためにAPI呼び出しを行いたいので、文字を入力するたびに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.Get)。

最後に、画面が閉じられたら、サブスクリプションを必ず破棄してください!

5
Justin

特にアプリケーションで既に使用している場合、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();
    });
}

キーイベントは、キーボードが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;            
    }
3
Rick Strahl

最新のヒットを単に覚えておいてください。

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

これにより、最後のイベントの後に十分な時間があれば、2番目のイベントが許可されます。

注:一連の高速イベントで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; 
}
1
DrKoch

私はこのパーティーに数十万分遅れていることは知っていますが、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*/
}
_
0
HDL_CinC_Dragon

クラス定義でこれを思いつきました。

一定時間(この例では3秒)アクションがなかった場合、すぐにアクションを実行したかったのです。

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

したがって、時計を4分の1秒ごとに細分化した場合

  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秒間蓄積されてから、最終的にガベージコレクションに利用できることに注意してください。

0
Anthony Wieser