Async command z limitem wykonań
W archiwum kodu znalazłem taki fragment – klasa LimitedRepetCommand odpowiedzialna za ograniczenie częstotliwości wykonywania jakiejś akcji.
Co nam daje ten kod?
- pierwsze żądanie jest wykonywane natychmiast w kontekście wątku właściciela klasy
- jeśli w czasie Δ od pierwszego wywołania zostanie zgłoszone kolejne wywołanie, czeka ono do końca okna czasowego Δ. Gdy przyjdzie jego czas, będzie wywołane w kontekście właściciela obiektów, co implikuje, że będzie w stanie manipulować GUI na takich samych prawach, jak wątek tworzący obiekt,
- gdy podczas trwania czasu oczekiwania Δ pojawi się więcej niż jedno żądanie, wówczas po upływie czasu Δ tylko raz wykonamy kontrolną funkcję
- jeśli po upływie czasu Δ od pierwszego wywołania pojawi się nowe żądanie, to jest wykonywane natychmiast
- jeśli metoda wywoływana wyrzuci wyjątek, zostanie on przechwycony i przekazany do metody obsługi
Przykład zastosowania
- ograniczenie odświeżeń widoku aplikacji
- w grze video gracz nie, możne strzelać częściej niż co 5 sekund, jeśli jednak po oddanym strzale wyda kilka rozkazów strzału, to wykonany będzie tylko jeden po 5 sekundach.
Naiwna implementacja (np. taka ->) jest błędna.
public void Execute()
{
var elapsedTime = (DateTime.Now - _lastExecution).TotalMilliseconds;
if (elapsedTime < _maxPeriod)
return;
_lastExecution = DateTime.Now;
try
{
_task.Invoke();
}
catch (Exception e)
{
_errorTask.Invoke(e);
}
}
Implementacja
Wywołanie zgłoszenia w czasie oczekiwania będzie zignorowane, a my chcemy, aby wykonało się na końcu czasu oczekiwania Δ. Kod spełniający warunek znajduje się poniżej. Klasa SynchronizationContext pozwalającą wykonać daną funkcję w kontekście innego wątku.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Demo.Commands
{
class LimitedRepetCommand
{
public LimitedRepetCommand(Action task, Action<Exception> errorTask, int period)
{
_contex = SynchronizationContext.Current;
_task = task;
_errorTask = errorTask;
_maxPeriod = period;
if (_task == null) throw new ArgumentNullException();
_timer = new Timer(TimerCb, null, Timeout.Infinite, Timeout.Infinite);
}
public void Execute()
{
var elapsedTime = (DateTime.Now - _lastExecution).TotalMilliseconds;
if (elapsedTime < _maxPeriod)
{
var delay = (_maxPeriod - elapsedTime);
delay = Math.Max(delay, 0);
_timer.Change((int)delay, Timeout.Infinite);
return;
}
TimerCb(null);
}
public void Cancel()
{
_timer.Change(Timeout.Infinite, Timeout.Infinite);
}
//=======================================
private void InvokeTask(Action task)
{
if (_contex == SynchronizationContext.Current)
{
task.Invoke();
return;
}
_contex.Send(b => task.Invoke(), null);
}
private void TimerCb(object state)
{
_lastExecution = DateTime.Now;
try
{
InvokeTask(_task);
}
catch (Exception e)
{
if (_errorTask != null)
InvokeTask(() => _errorTask.Invoke(e));
}
}
private readonly SynchronizationContext _contex;
private readonly Action _task;
private readonly Action<Exception> _errorTask;
private readonly int _maxPeriod;
private readonly Timer _timer;
private DateTime _lastExecution = DateTime.MinValue;
}
}
Testy jednostkowe
using System;
using System.Threading;
using System.Windows.Forms;
using Demo.Commands;
using NUnit.Framework;
namespace Demo.Tests
{
[TestFixture]
public class LimitedRepeatCommandTests
{
[Test]
public void Test1()
{
var state = 0;
var obj = new LimitedRepetCommand(() => ++state, null, 50);
obj.Execute();
Assert.AreEqual(1, state);
obj.Execute();
Assert.AreEqual(1, state);
obj.Execute();
Assert.AreEqual(1, state);
Thread.Sleep(100);
Assert.AreEqual(2, state);
Thread.Sleep(100);
Assert.AreEqual(2, state);
}
[Test]
public void Test2()
{
var state = 0;
var obj = new LimitedRepetCommand(() => ++state, null, 50);
obj.Execute();
Assert.AreEqual(1, state);
obj.Execute();
obj.Execute();
obj.Execute();
obj.Cancel();
Assert.AreEqual(1, state);
Thread.Sleep(100);
Assert.AreEqual(1, state);
}
[Test]
public void Test3()
{
var state = 0;
var obj = new LimitedRepetCommand(() => ++state, null, 100);
obj.Execute();
Assert.AreEqual(1, state);
obj.Execute();
obj.Execute();
obj.Execute();
Thread.Sleep(70);
obj.Execute();
obj.Execute();
Assert.AreEqual(1, state);
Thread.Sleep(40);
Assert.AreEqual(2, state);
Thread.Sleep(100);
Assert.AreEqual(2, state);
}
[Test]
public void Test4()
{
var state = 0;
var obj = new LimitedRepetCommand(() => ++state, null, 50);
obj.Execute();
Assert.AreEqual(1, state);
obj.Execute();
obj.Execute();
obj.Execute();
Thread.Sleep(40);
obj.Execute();
obj.Cancel();
Assert.AreEqual(1, state);
Thread.Sleep(100);
Assert.AreEqual(1, state);
}
[Test]
public void TestError1()
{
bool errorProcessed = false;
var tesk = new Action(() => { throw new ApplicationException(); } );
var errorTask = new Action<Exception>(e => { errorProcessed = true; ; });
var obj = new LimitedRepetCommand(tesk, errorTask, 50);
obj.Execute();
Thread.Sleep(100);
Assert.AreEqual(errorProcessed, true);
}
}
}