スライダー等からの大量リクエストの、最新値のみを別スレッド処理する

下記のような、スライダーの存在するアプリケーションを作成し、スライダーの値が変わる度に何かの処理をするとします。
f:id:any-programming:20170622190819p:plain

もしこの処理が重たい場合、普通に実装しただけでは、UIのレスポンスが悪くなってしまいます。

slider.ValueChanged += (s, e) =>
    Thread.Sleep(1000); // 仮想的な重い処理

そこで処理を別スレッドに移動するのですが、下記のように実装してしまうと、大量のスレッドが走ってしまうこととなります。

slider.ValueChanged += (s, e) => 
    Task.Run(() => Thread.Sleep(1000));

スライダーの最新の値の処理結果しか利用しない場合、全リクエストを処理するのは無駄になります。

今回はこのような場合に利用できるクラスを実装してみたいと思います。

LatestTaskWorker

処理を行うスレッドが一つ存在し、スレッドが利用可能になったときに、PostされたActionのうち最新のものを処理するよう実装してあります。

BlockingCollectionを利用すると、lockを自前で書く必要がなく、要素が存在しない間はRead側がブロックされるので便利です。

Post時に既存のActionを削除することにより、最新のActionのみ処理されるようにしてあります。

class LatestTaskWorker
{
    BlockingCollection<Action> _actions = new BlockingCollection<Action>();
    public LatestTaskWorker() =>
        Task.Run(() =>
        {
            // 要素が存在しない場合はブロックされる
            foreach (var action in _actions.GetConsumingEnumerable())
                action();
        });
    public void Post(Action action)
    {
        // 既存のActionを削除(実際はTryTake一回で問題ないと思います)
        while (_actions.TryTake(out var a)) { }
        _actions.Add(action);
    }
}


利用例

下記の様に実装すると、スレッドが利用可能になった時点で最後にPostされたActionを処理するようになります。

これにより、UIをフリーズさせることなく、可能な限り処理を行いつつ、最新値は必ず処理されるようになります。

var worker = new LatestTaskWorker();
slider.ValueChanged += (s, e) =>
{
    worker.Post(() =>
    {
        Thread.Sleep(1000);
        Console.WriteLine($"{e.NewValue}");
    });
};


安全なスレッド終了対応

このクラスを利用した状態でアプリケーションを閉じると、処理中のActionは強制終了させられてしまいます。

きちんと処理の終了を待ちたい時用にFinish関数を追加しました。

アプリケーション終了前にFinishを呼べば、処理が終了するのを待つことが可能です。

class LatestTaskWorker
{
    BlockingCollection<Action> _actions = new BlockingCollection<Action>();
    Task _task;
    public LatestTaskWorker() =>
        _task = Task.Run(() =>
        {
            foreach (var action in _actions.GetConsumingEnumerable())
                action();
        });
    public void Post(Action action)
    {
        while (_actions.TryTake(out var a)) { }
        _actions.Add(action);
    }
    public void Finish()
    {
        _actions.CompleteAdding();
        _task.Wait();
    }
}