I am making a software to show a chronometer on screen and run certain actions at certain times defined by the user. I found a way to show the chronometer on an independent window by using a DispatcherTimer with a 16ms Interval (around 60FPS) but now I have to find a way to run those actions at the defined times.
I made a component QueueableStopwatch
which does the work. It works in the following way:
- Give it an array of
QueueAction
objects - Internally creates an array for the QueueActions to be ran once (sorted by interval)
- Internally creates another array for the QueueActions to be repeated
- It have an internal
Stopwatch
to count the time - The component have the methods
Start()
andStop()
Start()
starts the internal stopwatch and starts the "queue processing" loop on a separate threadStop()
stops the stopwatch and the "queue processing" loop stops by itself whenStopwatch.IsRunning
changes to false
The "queue processing" loop does the following:
- Keeps a reference to the next
QueueAction
to be ran. If the internalStopwatch.Elapsed
> to the referencedQueueAction.Interval
its ran and the reference updates to the next one in the run once QueueActions array - We run each QueueAction in the to be repeated array under the condition
Stopwatch.Elapsed / QueueAction.Interval - QueueAction.TimesExecuted >= 1
. If ran we increaseQueueAction.TimesExecuted
by one.
Is this solution good enough to be implemented as the "core" of an application running critical actions?
Can the usage of Stopwatch.IsRunning
end up in unexpected behavior as documented here?
This is the component code:
public class QueueAction
{
/// <summary>
/// Interval to run the action
/// </summary>
public TimeSpan Interval { get; set; }
/// <summary>
/// The current action to run
/// </summary>
public Action Action { get; set; }
/// <summary>
/// Dispatcher the action will be ran into
/// </summary>
public Dispatcher Dispatcher { get; set; }
/// <summary>
/// True if the action will be repeated
/// </summary>
public bool Repeat { get; set; }
}
public class QueueableStopwatch
{
private Stopwatch _stopwatch = new Stopwatch();
public TimeSpan Elapsed => _stopwatch.Elapsed;
private RepeatableQueueAction[] _repeatQueue = { };
private QueueAction[] _singleQueue = { };
public QueueAction[] Queue
{
get => _singleQueue;
set
{
_repeatQueue = value.Where(action => action.Repeat).Select(action => new RepeatableQueueAction { QueueAction = action }).ToArray();
_singleQueue = value.Where(action => !action.Repeat).OrderBy(action => action.Interval.TotalMilliseconds).ToArray();
}
}
public void Start()
{
if (_stopwatch.IsRunning)
throw new InvalidOperationException("The chronometer is already running");
_stopwatch.Start();
if(_singleQueue.Length > 0)
{
new Task(() =>
{
int i = 0;
QueueAction selectedAction = selectedAction = _singleQueue[i];
do
{
if (i < _singleQueue.Length && selectedAction.Interval <= _stopwatch.Elapsed) // Single time run queue
{
selectedAction.Dispatcher.Invoke(() => selectedAction.Action());
i ;
if(i < _singleQueue.Length)
selectedAction = _singleQueue[i];
}
foreach(var repetitionAction in _repeatQueue) // Repeat run queue
{
if(_stopwatch.Elapsed / repetitionAction.QueueAction.Interval - repetitionAction.Repetitions >= 1)
{
repetitionAction.QueueAction.Dispatcher.Invoke(() => repetitionAction.QueueAction.Action());
repetitionAction.Repetitions ;
}
}
}
while (_stopwatch.IsRunning);
}).Start();
}
}
public void Stop()
{
_stopwatch.Reset();
}
private class RepeatableQueueAction
{
public QueueAction QueueAction { get; set; }
public int Repetitions { get; set; }
}
}
If you want to run it this xaml does the work:
MainWindow.xaml
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800"
Background="Black">
<StackPanel Orientation="Vertical">
<Label Name="lblMessage" Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="56"/>
<Button Click="Button_Click" Content="Stop" HorizontalAlignment="Center"/>
</StackPanel>
</Window>
MainWindow.cs
public partial class MainWindow : Window
{
QueueableStopwatch stopwatch = new QueueableStopwatch();
public MainWindow()
{
InitializeComponent();
stopwatch.Queue = new QueueAction[]
{
new QueueAction
{
Dispatcher = lblMessage.Dispatcher,
Interval = TimeSpan.FromSeconds(7),
Action = () => lblMessage.Content = $"[{stopwatch.Elapsed}]I run every 7 seconds",
Repeat = true
},
new QueueAction
{
Dispatcher = lblMessage.Dispatcher,
Interval = TimeSpan.FromSeconds(10),
Action = () => lblMessage.Content = $"[{stopwatch.Elapsed}]Queued first but ran at 10 seconds"
},
new QueueAction
{
Dispatcher = lblMessage.Dispatcher,
Interval = TimeSpan.FromSeconds(3),
Action = () => lblMessage.Content = $"[{stopwatch.Elapsed}]3 seconds elapsed"
}
};
stopwatch.Start();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
stopwatch.Stop();
}
}
CodePudding user response:
A stopwatch is the wrong tool for the job, you are essentially spinwaiting on a thread, burning CPU time and potentially causing other threads to be starved.
The framework already provides this functionality in the form of timers. If you want to run actions on the UI thread a dispatch timer would be suitable. So for each action you want to schedule, create a corresponding timer. You might need a wrapper if you want to decide up front if the action should be repeated or not.
var timer = new DispatcherTimer(){ Interval = TimeSpan.FromSeconds(10) };
timer.Tick = (o, e) => {
lblMessage.Content = $"[{stopwatch.Elapsed}]Queued first but ran at 10 seconds"
timer.Stop();
};
timer.Start();
The resolution of timers depend on the OS, but typically 1-16ms. For a UI program this should be sufficient, there will be various small delays for the screen to render anyway. If you need better resolution there are mediatimer.