--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Threading
+{
+ /// <summary>Represents a timer that can have its due time and period changed.</summary>
+ /// <remarks>
+ /// Implementations of <see cref="Change"/>, <see cref="IDisposable.Dispose"/>, and <see cref="IAsyncDisposable.DisposeAsync"/>
+ /// must all be thread-safe such that the timer instance may be accessed concurrently from multiple threads.
+ /// </remarks>
+ public interface ITimer : IDisposable, IAsyncDisposable
+ {
+ /// <summary>Changes the start time and the interval between method invocations for a timer, using <see cref="TimeSpan"/> values to measure time intervals.</summary>
+ /// <param name="dueTime">
+ /// A <see cref="TimeSpan"/> representing the amount of time to delay before invoking the callback method specified when the <see cref="ITimer"/> was constructed.
+ /// Specify <see cref="Timeout.InfiniteTimeSpan"/> to prevent the timer from restarting. Specify <see cref="TimeSpan.Zero"/> to restart the timer immediately.
+ /// </param>
+ /// <param name="period">
+ /// The time interval between invocations of the callback method specified when the Timer was constructed.
+ /// Specify <see cref="Timeout.InfiniteTimeSpan"/> to disable periodic signaling.
+ /// </param>
+ /// <returns><see langword="true"/> if the timer was successfully updated; otherwise, <see langword="false"/>.</returns>
+ /// <exception cref="ArgumentOutOfRangeException">The <paramref name="dueTime"/> or <paramref name="period"/> parameter, in milliseconds, is less than -1 or greater than 4294967294.</exception>
+ /// <remarks>
+ /// It is the responsibility of the implementer of the ITimer interface to ensure thread safety.
+ /// </remarks>
+ bool Change(TimeSpan dueTime, TimeSpan period);
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace System
+{
+ /// <summary>Provides an abstraction for time.</summary>
+ public abstract class TimeProvider
+ {
+ private readonly double _timeToTicksRatio;
+
+ /// <summary>
+ /// Gets a <see cref="TimeProvider"/> that provides a clock based on <see cref="DateTimeOffset.UtcNow"/>,
+ /// a time zone based on <see cref="TimeZoneInfo.Local"/>, a high-performance time stamp based on <see cref="Stopwatch"/>,
+ /// and a timer based on <see cref="Timer"/>.
+ /// </summary>
+ /// <remarks>
+ /// If the <see cref="TimeZoneInfo.Local"/> changes after the object is returned, the change will be reflected in any subsequent operations that retrieve <see cref="TimeProvider.LocalNow"/>.
+ /// </remarks>
+ public static TimeProvider System { get; } = new SystemTimeProvider(null);
+
+ /// <summary>
+ /// Initializes the instance with the timestamp frequency.
+ /// </summary>
+ /// <exception cref="ArgumentOutOfRangeException">The value of <paramref name="timestampFrequency"/> is negative or zero.</exception>
+ /// <param name="timestampFrequency">Frequency of the values returned from <see cref="GetTimestamp"/> method.</param>
+ protected TimeProvider(long timestampFrequency)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(timestampFrequency);
+ TimestampFrequency = timestampFrequency;
+ _timeToTicksRatio = (double)TimeSpan.TicksPerSecond / TimestampFrequency;
+ }
+
+ /// <summary>
+ /// Gets a <see cref="DateTimeOffset"/> value whose date and time are set to the current
+ /// Coordinated Universal Time (UTC) date and time and whose offset is Zero,
+ /// all according to this <see cref="TimeProvider"/>'s notion of time.
+ /// </summary>
+ public abstract DateTimeOffset UtcNow { get; }
+
+ /// <summary>
+ /// Gets a <see cref="DateTimeOffset"/> value that is set to the current date and time according to this <see cref="TimeProvider"/>'s
+ /// notion of time based on <see cref="UtcNow"/>, with the offset set to the <see cref="LocalTimeZone"/>'s offset from Coordinated Universal Time (UTC).
+ /// </summary>
+ public DateTimeOffset LocalNow
+ {
+ get
+ {
+ DateTime utcDateTime = UtcNow.UtcDateTime;
+ TimeSpan offset = LocalTimeZone.GetUtcOffset(utcDateTime);
+
+ long localTicks = utcDateTime.Ticks + offset.Ticks;
+ if ((ulong)localTicks > DateTime.MaxTicks)
+ {
+ localTicks = localTicks < DateTime.MinTicks ? DateTime.MinTicks : DateTime.MaxTicks;
+ }
+
+ return new DateTimeOffset(localTicks, offset);
+ }
+ }
+
+ /// <summary>
+ /// Gets a <see cref="TimeZoneInfo"/> object that represents the local time zone according to this <see cref="TimeProvider"/>'s notion of time.
+ /// </summary>
+ public abstract TimeZoneInfo LocalTimeZone { get; }
+
+ /// <summary>
+ /// Gets the frequency of <see cref="GetTimestamp"/> of high-frequency value per second.
+ /// </summary>
+ public long TimestampFrequency { get; }
+
+ /// <summary>
+ /// Creates a <see cref="TimeProvider"/> that provides a clock based on <see cref="DateTimeOffset.UtcNow"/>,
+ /// a time zone based on <paramref name="timeZone"/>, a high-performance time stamp based on <see cref="Stopwatch"/>,
+ /// and a timer based on <see cref="Timer"/>.
+ /// </summary>
+ /// <param name="timeZone">The time zone to use in getting the local time using <see cref="LocalNow"/>. </param>
+ /// <returns>A new instance of <see cref="TimeProvider"/>. </returns>
+ /// <exception cref="ArgumentNullException"><paramref name="timeZone"/> is null.</exception>
+ public static TimeProvider FromLocalTimeZone(TimeZoneInfo timeZone)
+ {
+ ArgumentNullException.ThrowIfNull(timeZone);
+ return new SystemTimeProvider(timeZone);
+ }
+
+ /// <summary>
+ /// Gets the current high-frequency value designed to measure small time intervals with high accuracy in the timer mechanism.
+ /// </summary>
+ /// <returns>A long integer representing the high-frequency counter value of the underlying timer mechanism. </returns>
+ public abstract long GetTimestamp();
+
+ /// <summary>
+ /// Gets the elapsed time between two timestamps retrieved using <see cref="GetTimestamp"/>.
+ /// </summary>
+ /// <param name="startingTimestamp">The timestamp marking the beginning of the time period.</param>
+ /// <param name="endingTimestamp">The timestamp marking the end of the time period.</param>
+ /// <returns>A <see cref="TimeSpan"/> for the elapsed time between the starting and ending timestamps.</returns>
+ public TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) =>
+ new TimeSpan((long)((endingTimestamp - startingTimestamp) * _timeToTicksRatio));
+
+ /// <summary>Creates a new <see cref="ITimer"/> instance, using <see cref="TimeSpan"/> values to measure time intervals.</summary>
+ /// <param name="callback">
+ /// A delegate representing a method to be executed when the timer fires. The method specified for callback should be reentrant,
+ /// as it may be invoked simultaneously on two threads if the timer fires again before or while a previous callback is still being handled.
+ /// </param>
+ /// <param name="state">An object to be passed to the <paramref name="callback"/>. This may be null.</param>
+ /// <param name="dueTime">The amount of time to delay before <paramref name="callback"/> is invoked. Specify <see cref="Timeout.InfiniteTimeSpan"/> to prevent the timer from starting. Specify <see cref="TimeSpan.Zero"/> to start the timer immediately.</param>
+ /// <param name="period">The time interval between invocations of <paramref name="callback"/>. Specify <see cref="Timeout.InfiniteTimeSpan"/> to disable periodic signaling.</param>
+ /// <returns>
+ /// The newly created <see cref="ITimer"/> instance.
+ /// </returns>
+ /// <exception cref="ArgumentNullException"><paramref name="callback"/> is null.</exception>
+ /// <exception cref="ArgumentOutOfRangeException">The number of milliseconds in the value of <paramref name="dueTime"/> or <paramref name="period"/> is negative and not equal to <see cref="Timeout.Infinite"/>, or is greater than <see cref="int.MaxValue"/>.</exception>
+ /// <remarks>
+ /// <para>
+ /// The delegate specified by the callback parameter is invoked once after <paramref name="dueTime"/> elapses, and thereafter each time the <paramref name="period"/> time interval elapses.
+ /// </para>
+ /// <para>
+ /// If <paramref name="dueTime"/> is zero, the callback is invoked immediately. If <paramref name="dueTime"/> is -1 milliseconds, <paramref name="callback"/> is not invoked; the timer is disabled,
+ /// but can be re-enabled by calling the <see cref="ITimer.Change"/> method.
+ /// </para>
+ /// <para>
+ /// If <paramref name="period"/> is 0 or -1 milliseconds and <paramref name="dueTime"/> is positive, <paramref name="callback"/> is invoked once; the periodic behavior of the timer is disabled,
+ /// but can be re-enabled using the <see cref="ITimer.Change"/> method.
+ /// </para>
+ /// <para>
+ /// The return <see cref="ITimer"/> instance will be implicitly rooted while the timer is still scheduled.
+ /// </para>
+ /// <para>
+ /// <see cref="CreateTimer"/> captures the <see cref="ExecutionContext"/> and stores that with the <see cref="ITimer"/> for use in invoking <paramref name="callback"/>
+ /// each time it's called. That capture can be suppressed with <see cref="ExecutionContext.SuppressFlow"/>.
+ /// </para>
+ /// </remarks>
+ public abstract ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period);
+
+ /// <summary>
+ /// Provides a default implementation of <see cref="TimeProvider"/> based on <see cref="DateTimeOffset.UtcNow"/>,
+ /// <see cref="TimeZoneInfo.Local"/>, <see cref="Stopwatch"/>, and <see cref="Timer"/>.
+ /// </summary>
+ private sealed class SystemTimeProvider : TimeProvider
+ {
+ /// <summary>The time zone to treat as local. If null, <see cref="TimeZoneInfo.Local"/> is used.</summary>
+ private readonly TimeZoneInfo? _localTimeZone;
+
+ /// <summary>Initializes the instance.</summary>
+ /// <param name="localTimeZone">The time zone to treat as local. If null, <see cref="TimeZoneInfo.Local"/> is used.</param>
+ internal SystemTimeProvider(TimeZoneInfo? localTimeZone) : base(Stopwatch.Frequency) => _localTimeZone = localTimeZone;
+
+ /// <inheritdoc/>
+ public override TimeZoneInfo LocalTimeZone => _localTimeZone ?? TimeZoneInfo.Local;
+
+ /// <inheritdoc/>
+ public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period)
+ {
+ ArgumentNullException.ThrowIfNull(callback);
+ return new SystemTimeProviderTimer(dueTime, period, callback, state);
+ }
+
+ /// <inheritdoc/>
+ public override long GetTimestamp() => Stopwatch.GetTimestamp();
+
+ /// <inheritdoc/>
+ public override DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
+
+ /// <summary>Thin wrapper for a <see cref="TimerQueueTimer"/>.</summary>
+ /// <remarks>
+ /// We don't return a TimerQueueTimer directly as it implements IThreadPoolWorkItem and we don't
+ /// want it exposed in a way that user code could directly queue the timer to the thread pool.
+ /// We also use this instead of Timer because CreateTimer needs to return a timer that's implicitly
+ /// rooted while scheduled.
+ /// </remarks>
+ private sealed class SystemTimeProviderTimer : ITimer
+ {
+ private readonly TimerQueueTimer _timer;
+
+ public SystemTimeProviderTimer(TimeSpan dueTime, TimeSpan period, TimerCallback callback, object? state)
+ {
+ (uint duration, uint periodTime) = CheckAndGetValues(dueTime, period);
+ _timer = new TimerQueueTimer(callback, state, duration, periodTime, flowExecutionContext: true);
+ }
+
+ public bool Change(TimeSpan dueTime, TimeSpan period)
+ {
+ (uint duration, uint periodTime) = CheckAndGetValues(dueTime, period);
+ return _timer.Change(duration, periodTime);
+ }
+
+ public void Dispose() => _timer.Dispose();
+
+ public ValueTask DisposeAsync() => _timer.DisposeAsync();
+
+ private static (uint duration, uint periodTime) CheckAndGetValues(TimeSpan dueTime, TimeSpan periodTime)
+ {
+ long dueTm = (long)dueTime.TotalMilliseconds;
+ ArgumentOutOfRangeException.ThrowIfLessThan(dueTm, -1, nameof(dueTime));
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(dueTm, Timer.MaxSupportedTimeout, nameof(dueTime));
+
+ long periodTm = (long)periodTime.TotalMilliseconds;
+ ArgumentOutOfRangeException.ThrowIfLessThan(periodTm, -1, nameof(periodTime));
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(periodTm, Timer.MaxSupportedTimeout, nameof(periodTime));
+
+ return ((uint)dueTm, (uint)periodTm);
+ }
+ }
+ }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Xunit;
+
+namespace Tests.System
+{
+ public class TimeProviderTests
+ {
+ [Fact]
+ public void TestUtcSystemTime()
+ {
+ DateTimeOffset dto1 = DateTimeOffset.UtcNow;
+ DateTimeOffset providerDto = TimeProvider.System.UtcNow;
+ DateTimeOffset dto2 = DateTimeOffset.UtcNow;
+
+ Assert.InRange(providerDto.Ticks, dto1.Ticks, dto2.Ticks);
+ Assert.Equal(TimeSpan.Zero, providerDto.Offset);
+ }
+
+ [Fact]
+ public void TestLocalSystemTime()
+ {
+ DateTimeOffset dto1 = DateTimeOffset.Now;
+ DateTimeOffset providerDto = TimeProvider.System.LocalNow;
+ DateTimeOffset dto2 = DateTimeOffset.Now;
+
+ // Ensure there was no daylight saving shift during the test execution.
+ if (dto1.Offset == dto2.Offset)
+ {
+ Assert.InRange(providerDto.Ticks, dto1.Ticks, dto2.Ticks);
+ Assert.Equal(dto1.Offset, providerDto.Offset);
+ }
+ }
+
+ [Fact]
+ public void TestSystemProviderWithTimeZone()
+ {
+ Assert.Equal(TimeZoneInfo.Local.Id, TimeProvider.System.LocalTimeZone.Id);
+
+ TimeZoneInfo tzi = TimeZoneInfo.FindSystemTimeZoneById(OperatingSystem.IsWindows() ? "Pacific Standard Time" : "America/Los_Angeles");
+
+ TimeProvider tp = TimeProvider.FromLocalTimeZone(tzi);
+ Assert.Equal(tzi.Id, tp.LocalTimeZone.Id);
+
+ DateTimeOffset utcDto1 = DateTimeOffset.UtcNow;
+ DateTimeOffset localDto = tp.LocalNow;
+ DateTimeOffset utcDto2 = DateTimeOffset.UtcNow;
+
+ DateTimeOffset utcConvertedDto = TimeZoneInfo.ConvertTime(localDto, TimeZoneInfo.Utc);
+ Assert.InRange(utcConvertedDto.Ticks, utcDto1.Ticks, utcDto2.Ticks);
+ }
+
+ [Fact]
+ public void TestSystemTimestamp()
+ {
+ long timestamp1 = Stopwatch.GetTimestamp();
+ long providerTimestamp1 = TimeProvider.System.GetTimestamp();
+ long timestamp2 = Stopwatch.GetTimestamp();
+ Thread.Sleep(100);
+ long providerTimestamp2 = TimeProvider.System.GetTimestamp();
+
+ Assert.InRange(providerTimestamp1, timestamp1, timestamp2);
+ Assert.True(providerTimestamp2 > timestamp2);
+ Assert.Equal(Stopwatch.GetElapsedTime(providerTimestamp1, providerTimestamp2), TimeProvider.System.GetElapsedTime(providerTimestamp1, providerTimestamp2));
+
+ Assert.Equal(Stopwatch.Frequency, TimeProvider.System.TimestampFrequency);
+ }
+
+ public static IEnumerable<object[]> TimersProvidersData()
+ {
+ yield return new object[] { TimeProvider.System, 6000 };
+ yield return new object[] { new FastClock(), 3000 };
+ }
+
+ [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+ [MemberData(nameof(TimersProvidersData))]
+ public void TestProviderTimer(TimeProvider provider, int MaxMilliseconds)
+ {
+ TimerState state = new TimerState();
+
+ state.Timer = provider.CreateTimer(
+ stat =>
+ {
+ TimerState s = (TimerState)stat;
+ s.Counter++;
+
+ s.TotalTicks += DateTimeOffset.UtcNow.Ticks - s.UtcNow.Ticks;
+
+ switch (s.Counter)
+ {
+ case 2:
+ s.Period = 400;
+ s.Timer.Change(TimeSpan.FromMilliseconds(s.Period), TimeSpan.FromMilliseconds(s.Period));
+ break;
+
+ case 4:
+ s.TokenSource.Cancel();
+ s.Timer.Dispose();
+ break;
+ }
+
+ s.UtcNow = DateTimeOffset.UtcNow;
+ },
+ state,
+ TimeSpan.FromMilliseconds(state.Period), TimeSpan.FromMilliseconds(state.Period));
+
+ state.TokenSource.Token.WaitHandle.WaitOne(30000);
+ state.TokenSource.Dispose();
+
+ Assert.Equal(4, state.Counter);
+ Assert.Equal(400, state.Period);
+ Assert.True(MaxMilliseconds >= state.TotalTicks / TimeSpan.TicksPerMillisecond, $"The total fired periods {state.TotalTicks / TimeSpan.TicksPerMillisecond}ms expected not exceeding the expected max {MaxMilliseconds}");
+ }
+
+ [Fact]
+ public void FastClockTest()
+ {
+ FastClock fastClock = new FastClock();
+
+ for (int i = 0; i < 20; i++)
+ {
+ DateTimeOffset fastNow = fastClock.UtcNow;
+ DateTimeOffset now = DateTimeOffset.UtcNow;
+
+ Assert.True(fastNow > now, $"Expected {fastNow} > {now}");
+
+ fastNow = fastClock.LocalNow;
+ now = DateTimeOffset.Now;
+
+ Assert.True(fastNow > now, $"Expected {fastNow} > {now}");
+ }
+
+ Assert.Equal(TimeSpan.TicksPerSecond, fastClock.TimestampFrequency);
+
+ long stamp1 = fastClock.GetTimestamp();
+ long stamp2 = fastClock.GetTimestamp();
+
+ Assert.Equal(stamp2 - stamp1, fastClock.GetElapsedTime(stamp1, stamp2).Ticks);
+ }
+
+ public static IEnumerable<object[]> TimersProvidersListData()
+ {
+ yield return new object[] { TimeProvider.System };
+ yield return new object[] { new FastClock() };
+ }
+
+ [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+ [MemberData(nameof(TimersProvidersListData))]
+ public static void CancellationTokenSourceWithTimer(TimeProvider provider)
+ {
+ //
+ // Test out some int-based timeout logic
+ //
+ CancellationTokenSource cts = new CancellationTokenSource(Timeout.InfiniteTimeSpan, provider); // should be an infinite timeout
+ CancellationToken token = cts.Token;
+ ManualResetEventSlim mres = new ManualResetEventSlim(false);
+ CancellationTokenRegistration ctr = token.Register(() => mres.Set());
+
+ Assert.False(token.IsCancellationRequested,
+ "CancellationTokenSourceWithTimer: Cancellation signaled on infinite timeout (int)!");
+
+ cts.CancelAfter(1000000);
+
+ Assert.False(token.IsCancellationRequested,
+ "CancellationTokenSourceWithTimer: Cancellation signaled on super-long timeout (int) !");
+
+ cts.CancelAfter(1);
+
+ Debug.WriteLine("CancellationTokenSourceWithTimer: > About to wait on cancellation that should occur soon (int)... if we hang, something bad happened");
+
+ mres.Wait();
+
+ cts.Dispose();
+
+ //
+ // Test out some TimeSpan-based timeout logic
+ //
+ TimeSpan prettyLong = new TimeSpan(1, 0, 0);
+ cts = new CancellationTokenSource(prettyLong, provider);
+ token = cts.Token;
+ mres = new ManualResetEventSlim(false);
+ ctr = token.Register(() => mres.Set());
+
+ Assert.False(token.IsCancellationRequested,
+ "CancellationTokenSourceWithTimer: Cancellation signaled on super-long timeout (TimeSpan,1)!");
+
+ cts.CancelAfter(prettyLong);
+
+ Assert.False(token.IsCancellationRequested,
+ "CancellationTokenSourceWithTimer: Cancellation signaled on super-long timeout (TimeSpan,2) !");
+
+ cts.CancelAfter(new TimeSpan(1000));
+
+ Debug.WriteLine("CancellationTokenSourceWithTimer: > About to wait on cancellation that should occur soon (TimeSpan)... if we hang, something bad happened");
+
+ mres.Wait();
+
+ cts.Dispose();
+ }
+
+ [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+ [MemberData(nameof(TimersProvidersListData))]
+ public static void RunDelayTests(TimeProvider provider)
+ {
+ CancellationTokenSource cts = new CancellationTokenSource();
+ CancellationToken token = cts.Token;
+
+ // These should all complete quickly, with RAN_TO_COMPLETION status.
+ Task task1 = Task.Delay(new TimeSpan(0), provider);
+ Task task2 = Task.Delay(new TimeSpan(0), provider, token);
+
+ Debug.WriteLine("RunDelayTests: > Waiting for 0-delayed uncanceled tasks to complete. If we hang, something went wrong.");
+ try
+ {
+ Task.WaitAll(task1, task2);
+ }
+ catch (Exception e)
+ {
+ Assert.True(false, string.Format("RunDelayTests: > FAILED. Unexpected exception on WaitAll(simple tasks): {0}", e));
+ }
+
+ Assert.True(task1.Status == TaskStatus.RanToCompletion, " > FAILED. Expected Delay(TimeSpan(0), timeProvider) to run to completion");
+ Assert.True(task2.Status == TaskStatus.RanToCompletion, " > FAILED. Expected Delay(TimeSpan(0), timeProvider, uncanceledToken) to run to completion");
+
+ // This should take some time
+ Task task3 = Task.Delay(TimeSpan.FromMilliseconds(20000), provider);
+ Assert.False(task3.IsCompleted, "RunDelayTests: > FAILED. Delay(20000) appears to have completed too soon(1).");
+ Task t2 = Task.Delay(TimeSpan.FromMilliseconds(10));
+ Assert.False(task3.IsCompleted, "RunDelayTests: > FAILED. Delay(10000) appears to have completed too soon(2).");
+ }
+
+ [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+ [MemberData(nameof(TimersProvidersListData))]
+ public static async void RunWaitAsyncTests(TimeProvider provider)
+ {
+ CancellationTokenSource cts = new CancellationTokenSource();
+
+ var tcs1 = new TaskCompletionSource();
+ Task task1 = tcs1.Task.WaitAsync(TimeSpan.FromDays(1), provider);
+ Assert.False(task1.IsCompleted);
+ tcs1.SetResult();
+ await task1;
+
+ var tcs2 = new TaskCompletionSource();
+ Task task2 = tcs2.Task.WaitAsync(TimeSpan.FromDays(1), provider, cts.Token);
+ Assert.False(task2.IsCompleted);
+ tcs2.SetResult();
+ await task2;
+
+ var tcs3 = new TaskCompletionSource<int>();
+ Task<int> task3 = tcs3.Task.WaitAsync(TimeSpan.FromDays(1), provider);
+ Assert.False(task3.IsCompleted);
+ tcs3.SetResult(42);
+ Assert.Equal(42, await task3);
+
+ var tcs4 = new TaskCompletionSource<int>();
+ Task<int> task4 = tcs4.Task.WaitAsync(TimeSpan.FromDays(1), provider, cts.Token);
+ Assert.False(task4.IsCompleted);
+ tcs4.SetResult(42);
+ Assert.Equal(42, await task4);
+
+ using CancellationTokenSource cts1 = new CancellationTokenSource();
+ Task task5 = Task.Run(() => { while (!cts1.Token.IsCancellationRequested) { Thread.Sleep(10); } });
+ await Assert.ThrowsAsync<TimeoutException>(() => task5.WaitAsync(TimeSpan.FromMilliseconds(10), provider));
+ cts1.Cancel();
+ await task5;
+
+ using CancellationTokenSource cts2 = new CancellationTokenSource();
+ Task task6 = Task.Run(() => { while (!cts2.Token.IsCancellationRequested) { Thread.Sleep(10); } });
+ await Assert.ThrowsAsync<TimeoutException>(() => task6.WaitAsync(TimeSpan.FromMilliseconds(10), provider, cts2.Token));
+ cts1.Cancel();
+ await task5;
+
+ using CancellationTokenSource cts3 = new CancellationTokenSource();
+ Task<int> task7 = Task<int>.Run(() => { while (!cts3.Token.IsCancellationRequested) { Thread.Sleep(10); } return 100; });
+ await Assert.ThrowsAsync<TimeoutException>(() => task7.WaitAsync(TimeSpan.FromMilliseconds(10), provider));
+ cts3.Cancel();
+ Assert.Equal(100, await task7);
+
+ using CancellationTokenSource cts4 = new CancellationTokenSource();
+ Task<int> task8 = Task<int>.Run(() => { while (!cts4.Token.IsCancellationRequested) { Thread.Sleep(10); } return 200; });
+ await Assert.ThrowsAsync<TimeoutException>(() => task8.WaitAsync(TimeSpan.FromMilliseconds(10), provider, cts4.Token));
+ cts4.Cancel();
+ Assert.Equal(200, await task8);
+ }
+
+ [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+ [MemberData(nameof(TimersProvidersListData))]
+ public static async void PeriodicTimerTests(TimeProvider provider)
+ {
+ var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(1), provider);
+ Assert.True(await timer.WaitForNextTickAsync());
+
+ timer.Dispose();
+ Assert.False(timer.WaitForNextTickAsync().Result);
+
+ timer.Dispose();
+ Assert.False(timer.WaitForNextTickAsync().Result);
+ }
+
+ [Fact]
+ public static void NegativeTests()
+ {
+ Assert.Throws<ArgumentOutOfRangeException>(() => new FastClock(-1)); // negative frequency
+ Assert.Throws<ArgumentOutOfRangeException>(() => new FastClock(0)); // zero frequency
+ Assert.Throws<ArgumentNullException>(() => TimeProvider.FromLocalTimeZone(null));
+
+ Assert.Throws<ArgumentNullException>(() => TimeProvider.System.CreateTimer(null, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan));
+ Assert.Throws<ArgumentOutOfRangeException>(() => TimeProvider.System.CreateTimer(obj => { }, null, TimeSpan.FromMilliseconds(-2), Timeout.InfiniteTimeSpan));
+ Assert.Throws<ArgumentOutOfRangeException>(() => TimeProvider.System.CreateTimer(obj => { }, null, Timeout.InfiniteTimeSpan, TimeSpan.FromMilliseconds(-2)));
+
+ Assert.Throws<ArgumentNullException>(() => new CancellationTokenSource(Timeout.InfiniteTimeSpan, null));
+
+ Assert.Throws<ArgumentNullException>(() => new PeriodicTimer(TimeSpan.FromMilliseconds(1), null));
+ }
+
+ class TimerState
+ {
+ public TimerState()
+ {
+ Counter = 0;
+ Period = 300;
+ TotalTicks = 0;
+ UtcNow = DateTimeOffset.UtcNow;
+ TokenSource = new CancellationTokenSource();
+ }
+
+ public CancellationTokenSource TokenSource { get; set; }
+ public int Counter { get; set; }
+ public int Period { get; set; }
+ public DateTimeOffset UtcNow { get; set; }
+ public ITimer Timer { get; set; }
+ public long TotalTicks { get; set; }
+ };
+
+ // Clock that speeds up the reported time
+ class FastClock : TimeProvider
+ {
+ private long _minutesToAdd;
+ private TimeZoneInfo _zone;
+
+ public FastClock(long timestampFrequency = TimeSpan.TicksPerSecond, TimeZoneInfo? zone = null) : base(timestampFrequency)
+ {
+ _zone = zone ?? TimeZoneInfo.Local;
+ }
+
+ public override DateTimeOffset UtcNow
+ {
+ get
+ {
+ DateTimeOffset now = DateTimeOffset.UtcNow;
+
+ _minutesToAdd++;
+ long remainingTicks = (DateTimeOffset.MaxValue.Ticks - now.Ticks);
+
+ if (_minutesToAdd * TimeSpan.TicksPerMinute > remainingTicks)
+ {
+ _minutesToAdd = 0;
+ return now;
+ }
+
+ return now.AddMinutes(_minutesToAdd);
+ }
+ }
+
+ public override TimeZoneInfo LocalTimeZone => _zone;
+
+ public override long GetTimestamp() => UtcNow.Ticks;
+
+ public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period) =>
+ new FastTimer(callback, state, dueTime, period);
+ }
+
+ // Timer that fire faster
+ class FastTimer : ITimer
+ {
+ private Timer _timer;
+
+ public FastTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period)
+ {
+ if (dueTime != Timeout.InfiniteTimeSpan)
+ {
+ dueTime = new TimeSpan(dueTime.Ticks / 2);
+ }
+
+ if (period != Timeout.InfiniteTimeSpan)
+ {
+ period = new TimeSpan(period.Ticks / 2);
+ }
+
+ _timer = new Timer(callback, state, dueTime, period);
+ }
+
+ public bool Change(TimeSpan dueTime, TimeSpan period)
+ {
+ if (dueTime != Timeout.InfiniteTimeSpan)
+ {
+ dueTime = new TimeSpan(dueTime.Ticks / 2);
+ }
+
+ if (period != Timeout.InfiniteTimeSpan)
+ {
+ period = new TimeSpan(period.Ticks / 2);
+ }
+
+ return _timer.Change(dueTime, period);
+ }
+
+ public void Dispose() => _timer.Dispose();
+ public ValueTask DisposeAsync() => _timer.DisposeAsync();
+ }
+ }
+}
/// <summary>Sets <see cref="_cleaningTimer"/> and <see cref="_timerIsRunning"/> based on the specified timeout.</summary>
private void SetCleaningTimer(TimeSpan timeout)
{
- try
+ if (_cleaningTimer!.Change(timeout, Timeout.InfiniteTimeSpan))
{
- _cleaningTimer!.Change(timeout, Timeout.InfiniteTimeSpan);
_timerIsRunning = timeout != Timeout.InfiniteTimeSpan;
}
- catch (ObjectDisposedException)
- {
- // In a rare race condition where the timer callback was queued
- // or executed and then the pool manager was disposed, the timer
- // would be disposed and then calling Change on it could result
- // in an ObjectDisposedException. We simply eat that.
- }
}
/// <summary>Removes unusable connections from each pool, and removes stale pools entirely.</summary>
<Compile Include="$(CommonPath)SkipLocalsInit.cs">
<Link>Common\SkipLocalsInit.cs</Link>
</Compile>
+ <Compile Include="$(CommonPath)System\ITimer.cs">
+ <Link>Common\System\ITimer.cs</Link>
+ </Compile>
+ <Compile Include="$(CommonPath)System\TimeProvider.cs">
+ <Link>Common\System\TimeProvider.cs</Link>
+ </Compile>
<Compile Include="$(CommonPath)System\LocalAppContextSwitches.Common.cs">
<Link>Common\System\LocalAppContextSwitches.Common.cs</Link>
</Compile>
private volatile int _state;
/// <summary>Whether this <see cref="CancellationTokenSource"/> has been disposed.</summary>
private bool _disposed;
- /// <summary>TimerQueueTimer used by CancelAfter and Timer-related ctors. Used instead of Timer to avoid extra allocations and because the rooted behavior is desired.</summary>
- private volatile TimerQueueTimer? _timer;
+ /// <summary>ITimer used by CancelAfter and Timer-related ctors. Used instead of Timer to avoid extra allocations and because the rooted behavior is desired.</summary>
+ private volatile ITimer? _timer;
/// <summary><see cref="System.Threading.WaitHandle"/> lazily initialized and returned from <see cref="WaitHandle"/>.</summary>
private volatile ManualResetEvent? _kernelEvent;
/// <summary>Registration state for the source.</summary>
/// canceled already.
/// </para>
/// </remarks>
- public CancellationTokenSource(TimeSpan delay)
+ public CancellationTokenSource(TimeSpan delay) : this(delay, TimeProvider.System)
{
+ }
+
+ /// <summary>Initializes a new instance of the <see cref="CancellationTokenSource"/> class that will be canceled after the specified <see cref="TimeSpan"/>.</summary>
+ /// <param name="delay">The time interval to wait before canceling this <see cref="CancellationTokenSource"/>.</param>
+ /// <param name="timeProvider">The <see cref="TimeProvider"/> with which to interpret the <paramref name="delay"/>.</param>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="delay"/>'s <see cref="TimeSpan.TotalMilliseconds"/> is less than -1 or greater than <see cref="uint.MaxValue"/> - 1.</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="timeProvider"/> is null.</exception>
+ /// <remarks>
+ /// The countdown for the delay starts during the call to the constructor. When the delay expires,
+ /// the constructed <see cref="CancellationTokenSource"/> is canceled, if it has
+ /// not been canceled already. Subsequent calls to CancelAfter will reset the delay for the constructed
+ /// <see cref="CancellationTokenSource"/>, if it has not been canceled already.
+ /// </remarks>
+ public CancellationTokenSource(TimeSpan delay, TimeProvider timeProvider)
+ {
+ ArgumentNullException.ThrowIfNull(timeProvider);
long totalMilliseconds = (long)delay.TotalMilliseconds;
if (totalMilliseconds < -1 || totalMilliseconds > Timer.MaxSupportedTimeout)
{
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.delay);
}
- InitializeWithTimer((uint)totalMilliseconds);
+ InitializeWithTimer((uint)totalMilliseconds, timeProvider);
}
/// <summary>
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.millisecondsDelay);
}
- InitializeWithTimer((uint)millisecondsDelay);
+ InitializeWithTimer((uint)millisecondsDelay, TimeProvider.System);
}
/// <summary>
/// Common initialization logic when constructing a CTS with a delay parameter.
/// A zero delay will result in immediate cancellation.
/// </summary>
- private void InitializeWithTimer(uint millisecondsDelay)
+ private void InitializeWithTimer(uint millisecondsDelay, TimeProvider timeProvider)
{
if (millisecondsDelay == 0)
{
}
else
{
- _timer = new TimerQueueTimer(s_timerCallback, this, millisecondsDelay, Timeout.UnsignedInfinite, flowExecutionContext: false);
-
+ if (timeProvider == TimeProvider.System)
+ {
+ _timer = new TimerQueueTimer(s_timerCallback, this, millisecondsDelay, Timeout.UnsignedInfinite, flowExecutionContext: false);
+ }
+ else
+ {
+ using (ExecutionContext.SuppressFlow())
+ {
+ _timer = timeProvider.CreateTimer(s_timerCallback, this, TimeSpan.FromMilliseconds(millisecondsDelay), Timeout.InfiniteTimeSpan);
+ }
+ }
// The timer roots this CTS instance while it's scheduled. That is by design, so
// that code like:
// new CancellationTokenSource(timeout).Token.Register(() => ...);
// expired and Disposed itself). But this would be considered bad behavior, as
// Dispose() is not thread-safe and should not be called concurrently with CancelAfter().
- TimerQueueTimer? timer = _timer;
+ ITimer? timer = _timer;
if (timer == null)
{
// Lazily initialize the timer in a thread-safe fashion.
// chance on a timer "losing" the initialization and then
// cancelling the token before it (the timer) can be disposed.
timer = new TimerQueueTimer(s_timerCallback, this, Timeout.UnsignedInfinite, Timeout.UnsignedInfinite, flowExecutionContext: false);
- TimerQueueTimer? currentTimer = Interlocked.CompareExchange(ref _timer, timer, null);
+ ITimer? currentTimer = Interlocked.CompareExchange(ref _timer, timer, null);
if (currentTimer != null)
{
// We did not initialize the timer. Dispose the new timer.
- timer.Close();
+ timer.Dispose();
timer = currentTimer;
}
}
- timer.Change(millisecondsDelay, Timeout.UnsignedInfinite, throwIfDisposed: false);
+ timer.Change(TimeSpan.FromMilliseconds(millisecondsDelay), Timeout.InfiniteTimeSpan);
}
/// <summary>
// to reset it to be infinite so that it won't fire, and then recognize that it could have already
// fired by the time we successfully changed it, and so check to see whether that's possibly the case.
// If we successfully reset it and it never fired, then we can be sure it won't trigger cancellation.
- bool reset =
- _timer is not TimerQueueTimer timer ||
- (timer.Change(Timeout.UnsignedInfinite, Timeout.UnsignedInfinite, throwIfDisposed: false) && !timer._everQueued);
+ bool reset = _timer is null ||
+ (_timer is TimerQueueTimer timer && timer.Change(Timeout.UnsignedInfinite, Timeout.UnsignedInfinite) && !timer._everQueued);
if (reset)
{
// internal source of cancellation, then Disposes of that linked source, which could
// happen at the same time the external entity is requesting cancellation).
- TimerQueueTimer? timer = _timer;
+ ITimer? timer = _timer;
if (timer != null)
{
_timer = null;
- timer.Close(); // TimerQueueTimer.Close is thread-safe
+ timer.Dispose(); // ITimer.Dispose is thread-safe
}
_registrations = null; // allow the GC to clean up registrations
if (!IsCancellationRequested &&
Interlocked.CompareExchange(ref _state, NotifyingState, NotCanceledState) == NotCanceledState)
{
- // Dispose of the timer, if any. Dispose may be running concurrently here, but TimerQueueTimer.Close is thread-safe.
- TimerQueueTimer? timer = _timer;
+ // Dispose of the timer, if any. Dispose may be running concurrently here, but ITimer.Dispose is thread-safe.
+ ITimer? timer = _timer;
if (timer != null)
{
_timer = null;
- timer.Close();
+ timer.Dispose();
}
// Set the event if it's been lazily initialized and hasn't yet been disposed of. Dispose may
public sealed class PeriodicTimer : IDisposable
{
/// <summary>The underlying timer.</summary>
- private readonly TimerQueueTimer _timer;
+ private readonly ITimer _timer;
/// <summary>All state other than the _timer, so that the rooted timer's callback doesn't indirectly root itself by referring to _timer.</summary>
private readonly State _state;
+ /// <summary>The timer's current period.</summary>
+ private TimeSpan _period;
/// <summary>Initializes the timer.</summary>
/// <param name="period">The period between ticks</param>
throw new ArgumentOutOfRangeException(nameof(period));
}
+ _period = period;
_state = new State();
+
_timer = new TimerQueueTimer(s => ((State)s!).Signal(), _state, ms, ms, flowExecutionContext: false);
}
+ /// <summary>Initializes the timer.</summary>
+ /// <param name="period">The period between ticks</param>
+ /// <param name="timeProvider">The <see cref="TimeProvider"/> used to interpret <paramref name="period"/>.</param>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="period"/> must be <see cref="Timeout.InfiniteTimeSpan"/> or represent a number of milliseconds equal to or larger than 1 and smaller than <see cref="uint.MaxValue"/>.</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="timeProvider"/> is null</exception>
+ public PeriodicTimer(TimeSpan period, TimeProvider timeProvider)
+ {
+ if (!TryGetMilliseconds(period, out uint ms))
+ {
+ GC.SuppressFinalize(this);
+ throw new ArgumentOutOfRangeException(nameof(period));
+ }
+
+ if (timeProvider is null)
+ {
+ GC.SuppressFinalize(this);
+ throw new ArgumentNullException(nameof(timeProvider));
+ }
+
+ _period = period;
+ _state = new State();
+ TimerCallback callback = s => ((State)s!).Signal();
+
+ if (timeProvider == TimeProvider.System)
+ {
+ _timer = new TimerQueueTimer(callback, _state, ms, ms, flowExecutionContext: false);
+ }
+ else
+ {
+ using (ExecutionContext.SuppressFlow())
+ {
+ _timer = timeProvider.CreateTimer(callback, _state, period, period);
+ }
+ }
+ }
+
/// <summary>Gets or sets the period between ticks.</summary>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="value"/> must be <see cref="Timeout.InfiniteTimeSpan"/> or represent a number of milliseconds equal to or larger than 1 and smaller than <see cref="uint.MaxValue"/>.</exception>
/// <remarks>
/// </remarks>
public TimeSpan Period
{
- get => _timer._period == Timeout.UnsignedInfinite ? Timeout.InfiniteTimeSpan : TimeSpan.FromMilliseconds(_timer._period);
+ get => _period;
set
{
- if (!TryGetMilliseconds(value, out uint ms))
+ if (!TryGetMilliseconds(value, out _))
{
throw new ArgumentOutOfRangeException(nameof(value));
}
- _timer.Change(ms, ms);
+ _period = value;
+ if (!_timer.Change(value, value))
+ {
+ ThrowHelper.ThrowObjectDisposedException(this);
+ }
}
}
public void Dispose()
{
GC.SuppressFinalize(this);
- _timer.Close();
+ _timer.Dispose();
_state.Signal(stopping: true);
}
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for a cancellation request.</param>
/// <returns>The <see cref="Task{TResult}"/> representing the asynchronous wait. It may or may not be the same instance as the current instance.</returns>
public new Task<TResult> WaitAsync(CancellationToken cancellationToken) =>
- WaitAsync(Timeout.UnsignedInfinite, cancellationToken);
+ WaitAsync(Timeout.UnsignedInfinite, TimeProvider.System, cancellationToken);
/// <summary>Gets a <see cref="Task{TResult}"/> that will complete when this <see cref="Task{TResult}"/> completes or when the specified timeout expires.</summary>
/// <param name="timeout">The timeout after which the <see cref="Task"/> should be faulted with a <see cref="TimeoutException"/> if it hasn't otherwise completed.</param>
/// <returns>The <see cref="Task{TResult}"/> representing the asynchronous wait. It may or may not be the same instance as the current instance.</returns>
public new Task<TResult> WaitAsync(TimeSpan timeout) =>
- WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), default);
+ WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), TimeProvider.System, default);
+
+ /// <summary>
+ /// Gets a <see cref="Task{TResult}"/> that will complete when this <see cref="Task{TResult}"/> completes or when the specified timeout expires.
+ /// </summary>
+ /// <param name="timeout">The timeout after which the <see cref="Task"/> should be faulted with a <see cref="TimeoutException"/> if it hasn't otherwise completed.</param>
+ /// <param name="timeProvider">The <see cref="TimeProvider"/> with which to interpret <paramref name="timeout"/>.</param>
+ /// <returns>The <see cref="Task{TResult}"/> representing the asynchronous wait. It may or may not be the same instance as the current instance.</returns>
+ public new Task<TResult> WaitAsync(TimeSpan timeout, TimeProvider timeProvider)
+ {
+ ArgumentNullException.ThrowIfNull(timeProvider);
+ return WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), timeProvider, default);
+ }
/// <summary>Gets a <see cref="Task{TResult}"/> that will complete when this <see cref="Task{TResult}"/> completes, when the specified timeout expires, or when the specified <see cref="CancellationToken"/> has cancellation requested.</summary>
/// <param name="timeout">The timeout after which the <see cref="Task"/> should be faulted with a <see cref="TimeoutException"/> if it hasn't otherwise completed.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for a cancellation request.</param>
/// <returns>The <see cref="Task{TResult}"/> representing the asynchronous wait. It may or may not be the same instance as the current instance.</returns>
public new Task<TResult> WaitAsync(TimeSpan timeout, CancellationToken cancellationToken) =>
- WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), cancellationToken);
+ WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), TimeProvider.System, cancellationToken);
+
+ /// <summary>
+ /// Gets a <see cref="Task{TResult}"/> that will complete when this <see cref="Task{TResult}"/> completes, when the specified timeout expires, or when the specified <see cref="CancellationToken"/> has cancellation requested.
+ /// </summary>
+ /// <param name="timeout">The timeout after which the <see cref="Task"/> should be faulted with a <see cref="TimeoutException"/> if it hasn't otherwise completed.</param>
+ /// <param name="timeProvider">The <see cref="TimeProvider"/> with which to interpret <paramref name="timeout"/>.</param>
+ /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for a cancellation request.</param>
+ /// <returns>The <see cref="Task{TResult}"/> representing the asynchronous wait. It may or may not be the same instance as the current instance.</returns>
+ public new Task<TResult> WaitAsync(TimeSpan timeout, TimeProvider timeProvider, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(timeProvider);
+ return WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), timeProvider, cancellationToken);
+ }
- private Task<TResult> WaitAsync(uint millisecondsTimeout, CancellationToken cancellationToken)
+ private Task<TResult> WaitAsync(uint millisecondsTimeout, TimeProvider timeProvider, CancellationToken cancellationToken)
{
if (IsCompleted || (!cancellationToken.CanBeCanceled && millisecondsTimeout == Timeout.UnsignedInfinite))
{
return FromException<TResult>(new TimeoutException());
}
- return new CancellationPromise<TResult>(this, millisecondsTimeout, cancellationToken);
+ return new CancellationPromise<TResult>(this, millisecondsTimeout, timeProvider, cancellationToken);
}
#endregion
/// <summary>Gets a <see cref="Task"/> that will complete when this <see cref="Task"/> completes or when the specified <see cref="CancellationToken"/> has cancellation requested.</summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for a cancellation request.</param>
/// <returns>The <see cref="Task"/> representing the asynchronous wait. It may or may not be the same instance as the current instance.</returns>
- public Task WaitAsync(CancellationToken cancellationToken) => WaitAsync(Timeout.UnsignedInfinite, cancellationToken);
+ public Task WaitAsync(CancellationToken cancellationToken) => WaitAsync(Timeout.UnsignedInfinite, TimeProvider.System, cancellationToken);
/// <summary>Gets a <see cref="Task"/> that will complete when this <see cref="Task"/> completes or when the specified timeout expires.</summary>
/// <param name="timeout">The timeout after which the <see cref="Task"/> should be faulted with a <see cref="TimeoutException"/> if it hasn't otherwise completed.</param>
/// <returns>The <see cref="Task"/> representing the asynchronous wait. It may or may not be the same instance as the current instance.</returns>
- public Task WaitAsync(TimeSpan timeout) => WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), default);
+ public Task WaitAsync(TimeSpan timeout) => WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), TimeProvider.System, default);
+
+ /// <summary>Gets a <see cref="Task"/> that will complete when this <see cref="Task"/> completes or when the specified timeout expires.</summary>
+ /// <param name="timeout">The timeout after which the <see cref="Task"/> should be faulted with a <see cref="TimeoutException"/> if it hasn't otherwise completed.</param>
+ /// <param name="timeProvider">The <see cref="TimeProvider"/> with which to interpret <paramref name="timeout"/>.</param>
+ /// <returns>The <see cref="Task"/> representing the asynchronous wait. It may or may not be the same instance as the current instance.</returns>
+ /// <exception cref="System.ArgumentNullException">The <paramref name="timeProvider"/> argument is null.</exception>
+ public Task WaitAsync(TimeSpan timeout, TimeProvider timeProvider)
+ {
+ ArgumentNullException.ThrowIfNull(timeProvider);
+ return WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), timeProvider, default);
+ }
/// <summary>Gets a <see cref="Task"/> that will complete when this <see cref="Task"/> completes, when the specified timeout expires, or when the specified <see cref="CancellationToken"/> has cancellation requested.</summary>
/// <param name="timeout">The timeout after which the <see cref="Task"/> should be faulted with a <see cref="TimeoutException"/> if it hasn't otherwise completed.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for a cancellation request.</param>
/// <returns>The <see cref="Task"/> representing the asynchronous wait. It may or may not be the same instance as the current instance.</returns>
public Task WaitAsync(TimeSpan timeout, CancellationToken cancellationToken) =>
- WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), cancellationToken);
+ WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), TimeProvider.System, cancellationToken);
- private Task WaitAsync(uint millisecondsTimeout, CancellationToken cancellationToken)
+ /// <summary>Gets a <see cref="Task"/> that will complete when this <see cref="Task"/> completes, when the specified timeout expires, or when the specified <see cref="CancellationToken"/> has cancellation requested.</summary>
+ /// <param name="timeout">The timeout after which the <see cref="Task"/> should be faulted with a <see cref="TimeoutException"/> if it hasn't otherwise completed.</param>
+ /// <param name="timeProvider">The <see cref="TimeProvider"/> with which to interpret <paramref name="timeout"/>.</param>
+ /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for a cancellation request.</param>
+ /// <returns>The <see cref="Task"/> representing the asynchronous wait. It may or may not be the same instance as the current instance.</returns>
+ /// <exception cref="System.ArgumentNullException">The <paramref name="timeProvider"/> argument is null.</exception>
+ public Task WaitAsync(TimeSpan timeout, TimeProvider timeProvider, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(timeProvider);
+ return WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), timeProvider, cancellationToken);
+ }
+
+ private Task WaitAsync(uint millisecondsTimeout, TimeProvider timeProvider, CancellationToken cancellationToken)
{
if (IsCompleted || (!cancellationToken.CanBeCanceled && millisecondsTimeout == Timeout.UnsignedInfinite))
{
return FromException(new TimeoutException());
}
- return new CancellationPromise<VoidTaskResult>(this, millisecondsTimeout, cancellationToken);
+ return new CancellationPromise<VoidTaskResult>(this, millisecondsTimeout, timeProvider, cancellationToken);
}
/// <summary>Task that's completed when another task, timeout, or cancellation token triggers.</summary>
/// <summary>Cancellation registration used to unregister from the token source upon timeout or the task completing.</summary>
private readonly CancellationTokenRegistration _registration;
/// <summary>The timer used to implement the timeout. It's stored so that it's rooted and so that we can dispose it upon cancellation or the task completing.</summary>
- private readonly TimerQueueTimer? _timer;
+ private readonly ITimer? _timer;
- internal CancellationPromise(Task source, uint millisecondsDelay, CancellationToken token)
+ internal CancellationPromise(Task source, uint millisecondsDelay, TimeProvider timeProvider, CancellationToken token)
{
Debug.Assert(source != null);
Debug.Assert(millisecondsDelay != 0);
// Register with a timer if it's needed.
if (millisecondsDelay != Timeout.UnsignedInfinite)
{
- _timer = new TimerQueueTimer(static state =>
+ TimerCallback callback = static state =>
{
var thisRef = (CancellationPromise<TResult>)state!;
if (thisRef.TrySetException(new TimeoutException()))
{
thisRef.Cleanup();
}
- }, this, millisecondsDelay, Timeout.UnsignedInfinite, flowExecutionContext: false);
+ };
+
+ if (timeProvider == TimeProvider.System)
+ {
+ _timer = new TimerQueueTimer(callback, this, millisecondsDelay, Timeout.UnsignedInfinite, flowExecutionContext: false);
+ }
+ else
+ {
+ using (ExecutionContext.SuppressFlow())
+ {
+ _timer = timeProvider.CreateTimer(callback, this, TimeSpan.FromMilliseconds(millisecondsDelay), Timeout.InfiniteTimeSpan);
+ }
+ }
}
// Register with the cancellation token.
private void Cleanup()
{
_registration.Dispose();
- _timer?.Close();
+ _timer?.Dispose();
_task.RemoveContinuation(this);
}
}
/// <remarks>
/// After the specified time delay, the Task is completed in RanToCompletion state.
/// </remarks>
- public static Task Delay(TimeSpan delay) => Delay(delay, default);
+ public static Task Delay(TimeSpan delay) => Delay(delay, TimeProvider.System, default);
+
+ /// <summary>Creates a task that completes after a specified time interval.</summary>
+ /// <param name="delay">The <see cref="TimeSpan"/> to wait before completing the returned task, or <see cref="Timeout.InfiniteTimeSpan"/> to wait indefinitely.</param>
+ /// <param name="timeProvider">The <see cref="TimeProvider"/> with which to interpret <paramref name="delay"/>.</param>
+ /// <returns>A task that represents the time delay.</returns>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="delay"/> represents a negative time interval other than <see cref="Timeout.InfiniteTimeSpan"/>.</exception>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="delay"/>'s <see cref="TimeSpan.TotalMilliseconds"/> property is greater than 4294967294.</exception>
+ /// <exception cref="System.ArgumentNullException">The <paramref name="timeProvider"/> argument is null.</exception>
+ public static Task Delay(TimeSpan delay, TimeProvider timeProvider) => Delay(delay, timeProvider, default);
/// <summary>
/// Creates a Task that will complete after a time delay.
/// delay has expired.
/// </remarks>
public static Task Delay(TimeSpan delay, CancellationToken cancellationToken) =>
- Delay(ValidateTimeout(delay, ExceptionArgument.delay), cancellationToken);
+ Delay(delay, TimeProvider.System, cancellationToken);
+
+ /// <summary>Creates a cancellable task that completes after a specified time interval.</summary>
+ /// <param name="delay">The <see cref="TimeSpan"/> to wait before completing the returned task, or <see cref="Timeout.InfiniteTimeSpan"/> to wait indefinitely.</param>
+ /// <param name="timeProvider">The <see cref="TimeProvider"/> with which to interpret <paramref name="delay"/>.</param>
+ /// <param name="cancellationToken">A cancellation token to observe while waiting for the task to complete.</param>
+ /// <returns>A task that represents the time delay.</returns>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="delay"/> represents a negative time interval other than <see cref="Timeout.InfiniteTimeSpan"/>.</exception>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="delay"/>'s <see cref="TimeSpan.TotalMilliseconds"/> property is greater than 4294967294.</exception>
+ /// <exception cref="System.ArgumentNullException">The <paramref name="timeProvider"/> argument is null.</exception>
+ public static Task Delay(TimeSpan delay, TimeProvider timeProvider, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(timeProvider);
+ return Delay(ValidateTimeout(delay, ExceptionArgument.delay), timeProvider, cancellationToken);
+ }
/// <summary>
/// Creates a Task that will complete after a time delay.
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.millisecondsDelay, ExceptionResource.Task_Delay_InvalidMillisecondsDelay);
}
- return Delay((uint)millisecondsDelay, cancellationToken);
+ return Delay((uint)millisecondsDelay, TimeProvider.System, cancellationToken);
}
- private static Task Delay(uint millisecondsDelay, CancellationToken cancellationToken) =>
+ private static Task Delay(uint millisecondsDelay, TimeProvider timeProvider, CancellationToken cancellationToken) =>
cancellationToken.IsCancellationRequested ? FromCanceled(cancellationToken) :
millisecondsDelay == 0 ? CompletedTask :
- cancellationToken.CanBeCanceled ? new DelayPromiseWithCancellation(millisecondsDelay, cancellationToken) :
- new DelayPromise(millisecondsDelay);
+ cancellationToken.CanBeCanceled ? new DelayPromiseWithCancellation(millisecondsDelay, timeProvider, cancellationToken) :
+ new DelayPromise(millisecondsDelay, timeProvider);
internal static uint ValidateTimeout(TimeSpan timeout, ExceptionArgument argument)
{
private class DelayPromise : Task
{
private static readonly TimerCallback s_timerCallback = TimerCallback;
- private readonly TimerQueueTimer? _timer;
+ private readonly ITimer? _timer;
- internal DelayPromise(uint millisecondsDelay)
+ internal DelayPromise(uint millisecondsDelay, TimeProvider timeProvider)
{
Debug.Assert(millisecondsDelay != 0);
if (millisecondsDelay != Timeout.UnsignedInfinite) // no need to create the timer if it's an infinite timeout
{
- _timer = new TimerQueueTimer(s_timerCallback, this, millisecondsDelay, Timeout.UnsignedInfinite, flowExecutionContext: false);
+ if (timeProvider == TimeProvider.System)
+ {
+ _timer = new TimerQueueTimer(s_timerCallback, this, millisecondsDelay, Timeout.UnsignedInfinite, flowExecutionContext: false);
+ }
+ else
+ {
+ using (ExecutionContext.SuppressFlow())
+ {
+ _timer = timeProvider.CreateTimer(s_timerCallback, this, TimeSpan.FromMilliseconds(millisecondsDelay), Timeout.InfiniteTimeSpan);
+ }
+ }
+
if (IsCompleted)
{
// Handle rare race condition where the timer fires prior to our having stored it into the field, in which case
// the timer won't have been cleaned up appropriately. This call to close might race with the Cleanup call to Close,
// but Close is thread-safe and will be a nop if it's already been closed.
- _timer.Close();
+ _timer.Dispose();
}
}
}
}
}
- protected virtual void Cleanup() => _timer?.Close();
+ protected virtual void Cleanup() => _timer?.Dispose();
}
/// <summary>DelayPromise that also supports cancellation.</summary>
{
private readonly CancellationTokenRegistration _registration;
- internal DelayPromiseWithCancellation(uint millisecondsDelay, CancellationToken token) : base(millisecondsDelay)
+ internal DelayPromiseWithCancellation(uint millisecondsDelay, TimeProvider timeProvider, CancellationToken token) : base(millisecondsDelay, timeProvider)
{
Debug.Assert(token.CanBeCanceled);
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
+using System.Runtime.CompilerServices;
using System.Threading.Tasks;
namespace System.Threading
// A timer in our TimerQueue.
[DebuggerDisplay("{DisplayString,nq}")]
[DebuggerTypeProxy(typeof(TimerDebuggerTypeProxy))]
- internal sealed class TimerQueueTimer : IThreadPoolWorkItem
+ internal sealed class TimerQueueTimer : ITimer, IThreadPoolWorkItem
{
// The associated timer queue.
private readonly TimerQueue _associatedTimerQueue;
internal bool _everQueued;
private object? _notifyWhenNoCallbacksRunning; // may be either WaitHandle or Task
+ internal TimerQueueTimer(TimerCallback timerCallback, object? state, TimeSpan dueTime, TimeSpan period, bool flowExecutionContext) :
+ this(timerCallback, state, GetMilliseconds(dueTime), GetMilliseconds(period), flowExecutionContext)
+ {
+ }
+
+ private static uint GetMilliseconds(TimeSpan time, [CallerArgumentExpression("time")] string? parameter = null)
+ {
+ long tm = (long)time.TotalMilliseconds;
+ ArgumentOutOfRangeException.ThrowIfLessThan(tm, -1, parameter);
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(tm, Timer.MaxSupportedTimeout, parameter);
+ return (uint)tm;
+ }
+
internal TimerQueueTimer(TimerCallback timerCallback, object? state, uint dueTime, uint period, bool flowExecutionContext)
{
_timerCallback = timerCallback;
}
}
- internal bool Change(uint dueTime, uint period, bool throwIfDisposed = true)
+ public bool Change(TimeSpan dueTime, TimeSpan period) =>
+ Change(GetMilliseconds(dueTime), GetMilliseconds(period));
+
+ internal bool Change(uint dueTime, uint period)
{
bool success;
{
if (_canceled)
{
- ObjectDisposedException.ThrowIf(throwIfDisposed, this);
return false;
}
return success;
}
-
- public void Close()
+ public void Dispose()
{
lock (_associatedTimerQueue)
{
}
}
-
- public bool Close(WaitHandle toSignal)
+ public bool Dispose(WaitHandle toSignal)
{
Debug.Assert(toSignal != null);
return success;
}
- public ValueTask CloseAsync()
+ public ValueTask DisposeAsync()
{
lock (_associatedTimerQueue)
{
~TimerHolder()
{
- _timer.Close();
+ _timer.Dispose();
}
- public void Close()
+ public void Dispose()
{
- _timer.Close();
+ _timer.Dispose();
GC.SuppressFinalize(this);
}
- public bool Close(WaitHandle notifyObject)
+ public bool Dispose(WaitHandle notifyObject)
{
- bool result = _timer.Close(notifyObject);
+ bool result = _timer.Dispose(notifyObject);
GC.SuppressFinalize(this);
return result;
}
- public ValueTask CloseAsync()
+ public ValueTask DisposeAsync()
{
- ValueTask result = _timer.CloseAsync();
+ ValueTask result = _timer.DisposeAsync();
GC.SuppressFinalize(this);
return result;
}
[DebuggerDisplay("{DisplayString,nq}")]
[DebuggerTypeProxy(typeof(TimerQueueTimer.TimerDebuggerTypeProxy))]
- public sealed class Timer : MarshalByRefObject, IDisposable, IAsyncDisposable
+ public sealed class Timer : MarshalByRefObject, IDisposable, IAsyncDisposable, ITimer
{
internal const uint MaxSupportedTimeout = 0xfffffffe;
return _timer._timer.Change((uint)dueTime, (uint)period);
}
- public bool Change(TimeSpan dueTime, TimeSpan period)
- {
- return Change((long)dueTime.TotalMilliseconds, (long)period.TotalMilliseconds);
- }
+ public bool Change(TimeSpan dueTime, TimeSpan period) =>
+ _timer._timer.Change(dueTime, period);
[CLSCompliant(false)]
public bool Change(uint dueTime, uint period)
{
ArgumentNullException.ThrowIfNull(notifyObject);
- return _timer.Close(notifyObject);
+ return _timer.Dispose(notifyObject);
}
public void Dispose()
{
- _timer.Close();
+ _timer.Dispose();
}
public ValueTask DisposeAsync()
{
- return _timer.CloseAsync();
+ return _timer.DisposeAsync();
}
private string DisplayString => _timer._timer.DisplayString;
Friday = 5,
Saturday = 6,
}
+ public abstract class TimeProvider
+ {
+ public static TimeProvider System { get; }
+ protected TimeProvider(long timestampFrequency) { throw null; }
+ public abstract System.DateTimeOffset UtcNow { get; }
+ public System.DateTimeOffset LocalNow { get; }
+ public abstract System.TimeZoneInfo LocalTimeZone { get; }
+ public long TimestampFrequency { get; }
+ public static TimeProvider FromLocalTimeZone(System.TimeZoneInfo timeZone) { throw null; }
+ public abstract long GetTimestamp();
+ public TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) { throw null; }
+ public abstract System.Threading.ITimer CreateTimer(System.Threading.TimerCallback callback, object? state, System.TimeSpan dueTime, System.TimeSpan period);
+ }
public sealed partial class DBNull : System.IConvertible, System.Runtime.Serialization.ISerializable
{
internal DBNull() { }
public partial class CancellationTokenSource : System.IDisposable
{
public CancellationTokenSource() { }
+ public CancellationTokenSource(TimeSpan delay, TimeProvider timeProvider) { }
public CancellationTokenSource(int millisecondsDelay) { }
public CancellationTokenSource(System.TimeSpan delay) { }
public bool IsCancellationRequested { get { throw null; } }
public sealed partial class PeriodicTimer : System.IDisposable
{
public PeriodicTimer(System.TimeSpan period) { }
+ public PeriodicTimer(TimeSpan period, TimeProvider timeProvider) { }
public void Dispose() { }
~PeriodicTimer() { }
public System.TimeSpan Period { get { throw null; } set { } }
public const int Infinite = -1;
public static readonly System.TimeSpan InfiniteTimeSpan;
}
- public sealed partial class Timer : System.MarshalByRefObject, System.IAsyncDisposable, System.IDisposable
+ public interface ITimer : System.IDisposable, System.IAsyncDisposable
+ {
+ bool Change(System.TimeSpan dueTime, System.TimeSpan period);
+ }
+ public sealed partial class Timer : System.MarshalByRefObject, System.IAsyncDisposable, System.IDisposable, ITimer
{
public Timer(System.Threading.TimerCallback callback) { }
public Timer(System.Threading.TimerCallback callback, object? state, int dueTime, int period) { }
public static System.Threading.Tasks.Task Delay(int millisecondsDelay, System.Threading.CancellationToken cancellationToken) { throw null; }
public static System.Threading.Tasks.Task Delay(System.TimeSpan delay) { throw null; }
public static System.Threading.Tasks.Task Delay(System.TimeSpan delay, System.Threading.CancellationToken cancellationToken) { throw null; }
+ public static System.Threading.Tasks.Task Delay(System.TimeSpan delay, System.TimeProvider timeProvider) { throw null; }
+ public static System.Threading.Tasks.Task Delay(System.TimeSpan delay, System.TimeProvider timeProvider, System.Threading.CancellationToken cancellationToken) { throw null; }
public void Dispose() { }
protected virtual void Dispose(bool disposing) { }
public static System.Threading.Tasks.Task FromCanceled(System.Threading.CancellationToken cancellationToken) { throw null; }
public System.Threading.Tasks.Task WaitAsync(System.Threading.CancellationToken cancellationToken) { throw null; }
public System.Threading.Tasks.Task WaitAsync(System.TimeSpan timeout) { throw null; }
public System.Threading.Tasks.Task WaitAsync(System.TimeSpan timeout, System.Threading.CancellationToken cancellationToken) { throw null; }
+ public System.Threading.Tasks.Task WaitAsync(System.TimeSpan timeout, System.TimeProvider timeProvider) { throw null; }
+ public System.Threading.Tasks.Task WaitAsync(System.TimeSpan timeout, System.TimeProvider timeProvider, System.Threading.CancellationToken cancellationToken) { throw null; }
public static System.Threading.Tasks.Task WhenAll(System.Collections.Generic.IEnumerable<System.Threading.Tasks.Task> tasks) { throw null; }
public static System.Threading.Tasks.Task WhenAll(params System.Threading.Tasks.Task[] tasks) { throw null; }
public static System.Threading.Tasks.Task<TResult[]> WhenAll<TResult>(System.Collections.Generic.IEnumerable<System.Threading.Tasks.Task<TResult>> tasks) { throw null; }
public new System.Threading.Tasks.Task<TResult> WaitAsync(System.Threading.CancellationToken cancellationToken) { throw null; }
public new System.Threading.Tasks.Task<TResult> WaitAsync(System.TimeSpan timeout) { throw null; }
public new System.Threading.Tasks.Task<TResult> WaitAsync(System.TimeSpan timeout, System.Threading.CancellationToken cancellationToken) { throw null; }
+ public new System.Threading.Tasks.Task<TResult> WaitAsync(System.TimeSpan timeout, System.TimeProvider timeProvider) { throw null; }
+ public new System.Threading.Tasks.Task<TResult> WaitAsync(System.TimeSpan timeout, System.TimeProvider timeProvider, System.Threading.CancellationToken cancellationToken) { throw null; }
}
public static partial class TaskToAsyncResult
{
<!-- some tests require full ICU data, force it -->
<WasmIncludeFullIcuData>true</WasmIncludeFullIcuData>
-
+
<!-- The test is looking for debugger attributes we would have stripped with NativeAOT -->
<IlcKeepManagedDebuggerSupport>true</IlcKeepManagedDebuggerSupport>
</PropertyGroup>
- <!--
+ <!--
Add special trait for running local timezone validation tests on certain devices e.g. in CI to make sure the local timezone is correct and not UTC. Disable these tests otherwise.
-->
<PropertyGroup>
<Compile Include="$(CommonTestPath)System\Collections\TestBase.NonGeneric.cs" Link="Common\System\Collections\TestBase.NonGeneric.cs" />
<Compile Include="$(CommonTestPath)System\Diagnostics\DebuggerAttributes.cs" Link="Common\System\Diagnostics\DebuggerAttributes.cs" />
<Compile Include="$(CommonTestPath)Tests\System\StringTests.cs" Link="Common\System\StringTests.cs" />
+ <Compile Include="$(CommonTestPath)Tests\System\TimeProviderTests.cs" Link="Common\System\TimeProviderTests.cs" />
<Compile Include="$(CommonTestPath)System\Collections\IDictionary.NonGeneric.Tests.cs" Link="Common\System\Collections\IDictionary.NonGeneric.Tests.cs" />
<Compile Include="$(CommonTestPath)System\Collections\IList.NonGeneric.Tests.cs" Link="Common\System\Collections\IList.NonGeneric.Tests.cs" />
<Compile Include="$(CommonTestPath)System\Collections\ICollection.NonGeneric.Tests.cs" Link="Common\System\Collections\ICollection.NonGeneric.Tests.cs" />
<ItemGroup>
<ProjectReference Include="$(LibrariesProjectRoot)System.Text.RegularExpressions\gen\System.Text.RegularExpressions.Generator.csproj" ReferenceOutputAssembly="false" SetTargetFramework="TargetFramework=netstandard2.0" OutputItemType="Analyzer" />
-
+
<PackageReference Include="Moq" Version="$(MoqVersion)" />
<PackageReference Include="System.Runtime.Numerics.TestData" Version="$(SystemRuntimeNumericsTestDataVersion)" GeneratePathProperty="true" />
}
[Fact]
- public void Timer_Change_AfterDispose_Throws()
+ public void Timer_Change_AfterDispose_Test()
{
var t = new Timer(new TimerCallback(EmptyTimerTarget), null, 1, 1);
+ Assert.True(t.Change(1, 1));
t.Dispose();
- Assert.Throws<ObjectDisposedException>(() => t.Change(1, 1));
- Assert.Throws<ObjectDisposedException>(() => t.Change(1L, 1L));
- Assert.Throws<ObjectDisposedException>(() => t.Change(1u, 1u));
- Assert.Throws<ObjectDisposedException>(() => t.Change(TimeSpan.FromMilliseconds(1), TimeSpan.FromMilliseconds(1)));
+ Assert.False(t.Change(1, 1));
+ Assert.False(t.Change(1L, 1L));
+ Assert.False(t.Change(1u, 1u));
+ Assert.False(t.Change(TimeSpan.FromMilliseconds(1), TimeSpan.FromMilliseconds(1)));
}
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
{
var t = new Timer(_ => { });
await t.DisposeAsync();
- Assert.Throws<ObjectDisposedException>(() => t.Change(-1, -1));
+ Assert.False(t.Change(-1, -1));
}
[Fact]
t.Dispose(allTicksCompleted);
Assert.True(allTicksCompleted.WaitOne(MaxPositiveTimeoutInMs));
Assert.Equal(0, tickCount);
- Assert.Throws<ObjectDisposedException>(() => t.Change(0, 0));
+ Assert.False(t.Change(0, 0));
}
[OuterLoop("Incurs seconds delay to wait for events that should never happen")]