private readonly State _state;
/// <summary>Initializes the timer.</summary>
- /// <param name="period">The time interval between invocations of callback..</param>
+ /// <param name="period">The period between ticks</param>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="period"/> must represent a number of milliseconds equal to or larger than 1, and smaller than <see cref="uint.MaxValue"/>.</exception>
public PeriodicTimer(TimeSpan period)
{
- long ms = (long)period.TotalMilliseconds;
- if (ms < 1 || ms > Timer.MaxSupportedTimeout)
+ if (!TryGetMilliseconds(period, out uint ms))
{
GC.SuppressFinalize(this);
throw new ArgumentOutOfRangeException(nameof(period));
}
_state = new State();
- _timer = new TimerQueueTimer(s => ((State)s!).Signal(), _state, (uint)ms, (uint)ms, flowExecutionContext: false);
+ _timer = new TimerQueueTimer(s => ((State)s!).Signal(), _state, ms, ms, flowExecutionContext: false);
+ }
+
+ /// <summary>Gets or sets the period between ticks.</summary>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="value"/> must represent a number of milliseconds equal to or larger than 1, and smaller than <see cref="uint.MaxValue"/>.</exception>
+ /// <remarks>
+ /// All prior ticks of the timer, including any that may be waiting to be consumed by <see cref="WaitForNextTickAsync"/>,
+ /// are unaffected by changes to <see cref="Period"/>. Setting <see cref="Period"/> affects only and all subsequent times
+ /// at which the timer will tick.
+ /// </remarks>
+ public TimeSpan Period
+ {
+ get => TimeSpan.FromMilliseconds(_timer._period);
+ set
+ {
+ if (!TryGetMilliseconds(value, out uint ms))
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+
+ _timer.Change(ms, ms);
+ }
+ }
+
+ /// <summary>Tries to extract the number of milliseconds from <paramref name="value"/>.</summary>
+ /// <returns>
+ /// true if the number of milliseconds is extracted and stored into <paramref name="milliseconds"/>;
+ /// false if the number of milliseconds would be out of range of a timer.
+ /// </returns>
+ private static bool TryGetMilliseconds(TimeSpan value, out uint milliseconds)
+ {
+ long ms = (long)value.TotalMilliseconds;
+ if (ms >= 1 && ms <= Timer.MaxSupportedTimeout)
+ {
+ milliseconds = (uint)ms;
+ return true;
+ }
+
+ milliseconds = 0;
+ return false;
}
/// <summary>Wait for the next tick of the timer, or for the timer to be stopped.</summary>
}
[Fact]
+ public void Period_InvalidArguments_Throws()
+ {
+ PeriodicTimer timer = new PeriodicTimer(TimeSpan.FromMilliseconds(1));
+ AssertExtensions.Throws<ArgumentOutOfRangeException>("value", () => timer.Period = TimeSpan.FromMilliseconds(-1));
+ AssertExtensions.Throws<ArgumentOutOfRangeException>("value", () => timer.Period = TimeSpan.Zero);
+ AssertExtensions.Throws<ArgumentOutOfRangeException>("value", () => timer.Period = TimeSpan.FromMilliseconds(uint.MaxValue));
+
+ timer.Dispose();
+ Assert.Throws<ObjectDisposedException>(() => timer.Period = TimeSpan.FromMilliseconds(100));
+ }
+
+ [Fact]
+ public void Period_Roundtrips()
+ {
+ using PeriodicTimer timer = new PeriodicTimer(TimeSpan.FromMilliseconds(1));
+ Assert.Equal(TimeSpan.FromMilliseconds(1), timer.Period);
+
+ timer.Period = TimeSpan.FromDays(1);
+ Assert.Equal(TimeSpan.FromDays(1), timer.Period);
+
+ AssertExtensions.Throws<ArgumentOutOfRangeException>("value", () => timer.Period = TimeSpan.Zero);
+ Assert.Equal(TimeSpan.FromDays(1), timer.Period);
+ }
+
+ [Fact]
+ public async void Period_AffectsPendingWaits()
+ {
+ using PeriodicTimer timer = new PeriodicTimer(TimeSpan.FromDays(40));
+
+ ValueTask<bool> task = timer.WaitForNextTickAsync();
+ Assert.False(task.IsCompleted);
+
+ timer.Period = TimeSpan.FromMilliseconds(1);
+ await task;
+ }
+
+ [Fact]
public async Task Dispose_Idempotent()
{
var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(1));