Below is a custom UpdateablePeriodicTimer class, which is essentially a PeriodicTimer with a mutable Period property:
public class UpdateablePeriodicTimer : IDisposable
{
private readonly object _locker = new();
private PeriodicTimer _timer; // Becomes null when disposed
private PeriodicTimer _newTimer;
private TimeSpan _period;
private bool _waiting;
public UpdateablePeriodicTimer(TimeSpan period)
{
_timer = new(period);
_period = period;
}
public TimeSpan Period
{
get { lock (_locker) return _period; }
set
{
PeriodicTimer timerToDispose;
lock (_locker)
{
if (_timer is null) throw new ObjectDisposedException(null,
$"The {nameof(UpdateablePeriodicTimer)} has been disposed.");
if (_waiting)
{
timerToDispose = _newTimer;
_newTimer = new(value);
}
else
{
timerToDispose = _timer;
_timer = new(value);
}
_period = value;
}
timerToDispose?.Dispose();
}
}
public async ValueTask<bool> WaitForNextTickAsync(
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ValueTask<bool> waitTask;
lock (_locker)
{
if (_timer is null) return false; // Disposed
if (_waiting) throw new InvalidOperationException();
waitTask = _timer.WaitForNextTickAsync(cancellationToken);
_waiting = true;
}
try { return await waitTask.ConfigureAwait(false); }
finally
{
PeriodicTimer timerToDispose = null;
lock (_locker)
{
_waiting = false;
if (_timer is not null && _newTimer is not null)
{
timerToDispose = _timer;
_timer = _newTimer;
_newTimer = null;
}
}
timerToDispose?.Dispose();
}
}
public void Dispose()
{
PeriodicTimer timerToDispose;
lock (_locker)
{
if (_timer is null) return; // Disposed
timerToDispose = _timer;
_timer = null;
_newTimer?.Dispose();
}
timerToDispose.Dispose();
}
}
Changing the Period starts immediately a new PeriodicTimer, which is swapped with the current PeriodicTimer when the active WaitForNextTickAsync completes, or immediately if there is no active operation.
Online demo.
The UpdateablePeriodicTimer class is thread-safe.
A GitHub proposal for adding the Period property in the native PeriodicTimer, has been approved, and the implementation has been merged. So most likely it will be included in .NET 8. Changing the Period of the native PeriodicTimer will have different behavior than the UpdateablePeriodicTimer implementation above. Changing the PeriodicTimer.Period will reschedule the currently active WaitForNextTickAsync according to the new period. On the contrary changing the UpdateablePeriodicTimer.Period above does not affect the currently active WaitForNextTickAsync.