diff --git a/src/libraries/Common/src/System/ITimer.cs b/src/libraries/Common/src/System/ITimer.cs new file mode 100644 index 0000000000000..2dde8645f7b1c --- /dev/null +++ b/src/libraries/Common/src/System/ITimer.cs @@ -0,0 +1,29 @@ +// 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 +{ + /// Represents a timer that can have its due time and period changed. + /// + /// Implementations of , , and + /// must all be thread-safe such that the timer instance may be accessed concurrently from multiple threads. + /// + public interface ITimer : IDisposable, IAsyncDisposable + { + /// Changes the start time and the interval between method invocations for a timer, using values to measure time intervals. + /// + /// A representing the amount of time to delay before invoking the callback method specified when the was constructed. + /// Specify to prevent the timer from restarting. Specify to restart the timer immediately. + /// + /// + /// The time interval between invocations of the callback method specified when the Timer was constructed. + /// Specify to disable periodic signaling. + /// + /// if the timer was successfully updated; otherwise, . + /// The or parameter, in milliseconds, is less than -1 or greater than 4294967294. + /// + /// It is the responsibility of the implementer of the ITimer interface to ensure thread safety. + /// + bool Change(TimeSpan dueTime, TimeSpan period); + } +} diff --git a/src/libraries/Common/src/System/TimeProvider.cs b/src/libraries/Common/src/System/TimeProvider.cs new file mode 100644 index 0000000000000..316cde1dc3768 --- /dev/null +++ b/src/libraries/Common/src/System/TimeProvider.cs @@ -0,0 +1,210 @@ +// 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 +{ + /// Provides an abstraction for time. + public abstract class TimeProvider + { + private readonly double _timeToTicksRatio; + + /// + /// Gets a that provides a clock based on , + /// a time zone based on , a high-performance time stamp based on , + /// and a timer based on . + /// + /// + /// If the changes after the object is returned, the change will be reflected in any subsequent operations that retrieve . + /// + public static TimeProvider System { get; } = new SystemTimeProvider(null); + + /// + /// Initializes the instance with the timestamp frequency. + /// + /// The value of is negative or zero. + /// Frequency of the values returned from method. + protected TimeProvider(long timestampFrequency) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(timestampFrequency); + TimestampFrequency = timestampFrequency; + _timeToTicksRatio = (double)TimeSpan.TicksPerSecond / TimestampFrequency; + } + + /// + /// Gets a 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 's notion of time. + /// + public abstract DateTimeOffset UtcNow { get; } + + /// + /// Gets a value that is set to the current date and time according to this 's + /// notion of time based on , with the offset set to the 's offset from Coordinated Universal Time (UTC). + /// + 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); + } + } + + /// + /// Gets a object that represents the local time zone according to this 's notion of time. + /// + public abstract TimeZoneInfo LocalTimeZone { get; } + + /// + /// Gets the frequency of of high-frequency value per second. + /// + public long TimestampFrequency { get; } + + /// + /// Creates a that provides a clock based on , + /// a time zone based on , a high-performance time stamp based on , + /// and a timer based on . + /// + /// The time zone to use in getting the local time using . + /// A new instance of . + /// is null. + public static TimeProvider FromLocalTimeZone(TimeZoneInfo timeZone) + { + ArgumentNullException.ThrowIfNull(timeZone); + return new SystemTimeProvider(timeZone); + } + + /// + /// Gets the current high-frequency value designed to measure small time intervals with high accuracy in the timer mechanism. + /// + /// A long integer representing the high-frequency counter value of the underlying timer mechanism. + public abstract long GetTimestamp(); + + /// + /// Gets the elapsed time between two timestamps retrieved using . + /// + /// The timestamp marking the beginning of the time period. + /// The timestamp marking the end of the time period. + /// A for the elapsed time between the starting and ending timestamps. + public TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) => + new TimeSpan((long)((endingTimestamp - startingTimestamp) * _timeToTicksRatio)); + + /// Creates a new instance, using values to measure time intervals. + /// + /// 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. + /// + /// An object to be passed to the . This may be null. + /// The amount of time to delay before is invoked. Specify to prevent the timer from starting. Specify to start the timer immediately. + /// The time interval between invocations of . Specify to disable periodic signaling. + /// + /// The newly created instance. + /// + /// is null. + /// The number of milliseconds in the value of or is negative and not equal to , or is greater than . + /// + /// + /// The delegate specified by the callback parameter is invoked once after elapses, and thereafter each time the time interval elapses. + /// + /// + /// If is zero, the callback is invoked immediately. If is -1 milliseconds, is not invoked; the timer is disabled, + /// but can be re-enabled by calling the method. + /// + /// + /// If is 0 or -1 milliseconds and is positive, is invoked once; the periodic behavior of the timer is disabled, + /// but can be re-enabled using the method. + /// + /// + /// The return instance will be implicitly rooted while the timer is still scheduled. + /// + /// + /// captures the and stores that with the for use in invoking + /// each time it's called. That capture can be suppressed with . + /// + /// + public abstract ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period); + + /// + /// Provides a default implementation of based on , + /// , , and . + /// + private sealed class SystemTimeProvider : TimeProvider + { + /// The time zone to treat as local. If null, is used. + private readonly TimeZoneInfo? _localTimeZone; + + /// Initializes the instance. + /// The time zone to treat as local. If null, is used. + internal SystemTimeProvider(TimeZoneInfo? localTimeZone) : base(Stopwatch.Frequency) => _localTimeZone = localTimeZone; + + /// + public override TimeZoneInfo LocalTimeZone => _localTimeZone ?? TimeZoneInfo.Local; + + /// + public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period) + { + ArgumentNullException.ThrowIfNull(callback); + return new SystemTimeProviderTimer(dueTime, period, callback, state); + } + + /// + public override long GetTimestamp() => Stopwatch.GetTimestamp(); + + /// + public override DateTimeOffset UtcNow => DateTimeOffset.UtcNow; + + /// Thin wrapper for a . + /// + /// 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. + /// + 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); + } + } + } + } +} diff --git a/src/libraries/Common/tests/Tests/System/TimeProviderTests.cs b/src/libraries/Common/tests/Tests/System/TimeProviderTests.cs new file mode 100644 index 0000000000000..8b00756367d88 --- /dev/null +++ b/src/libraries/Common/tests/Tests/System/TimeProviderTests.cs @@ -0,0 +1,420 @@ +// 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 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 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(); + Task task3 = tcs3.Task.WaitAsync(TimeSpan.FromDays(1), provider); + Assert.False(task3.IsCompleted); + tcs3.SetResult(42); + Assert.Equal(42, await task3); + + var tcs4 = new TaskCompletionSource(); + Task 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(() => 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(() => task6.WaitAsync(TimeSpan.FromMilliseconds(10), provider, cts2.Token)); + cts1.Cancel(); + await task5; + + using CancellationTokenSource cts3 = new CancellationTokenSource(); + Task task7 = Task.Run(() => { while (!cts3.Token.IsCancellationRequested) { Thread.Sleep(10); } return 100; }); + await Assert.ThrowsAsync(() => task7.WaitAsync(TimeSpan.FromMilliseconds(10), provider)); + cts3.Cancel(); + Assert.Equal(100, await task7); + + using CancellationTokenSource cts4 = new CancellationTokenSource(); + Task task8 = Task.Run(() => { while (!cts4.Token.IsCancellationRequested) { Thread.Sleep(10); } return 200; }); + await Assert.ThrowsAsync(() => 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(() => new FastClock(-1)); // negative frequency + Assert.Throws(() => new FastClock(0)); // zero frequency + Assert.Throws(() => TimeProvider.FromLocalTimeZone(null)); + + Assert.Throws(() => TimeProvider.System.CreateTimer(null, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan)); + Assert.Throws(() => TimeProvider.System.CreateTimer(obj => { }, null, TimeSpan.FromMilliseconds(-2), Timeout.InfiniteTimeSpan)); + Assert.Throws(() => TimeProvider.System.CreateTimer(obj => { }, null, Timeout.InfiniteTimeSpan, TimeSpan.FromMilliseconds(-2))); + + Assert.Throws(() => new CancellationTokenSource(Timeout.InfiniteTimeSpan, null)); + + Assert.Throws(() => 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(); + } + } +} diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs index 9bc1af96d8835..db54690e4ac40 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs @@ -443,18 +443,10 @@ public void Dispose() /// Sets and based on the specified timeout. 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. - } } /// Removes unusable connections from each pool, and removes stale pools entirely. diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 6f2d52f360447..7804a67acb277 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -1317,6 +1317,12 @@ Common\SkipLocalsInit.cs + + Common\System\ITimer.cs + + + Common\System\TimeProvider.cs + Common\System\LocalAppContextSwitches.Common.cs diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/CancellationTokenSource.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/CancellationTokenSource.cs index b40c9033f865b..d6277386bd1c7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/CancellationTokenSource.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/CancellationTokenSource.cs @@ -38,8 +38,8 @@ private static void TimerCallback(object? state) => // separated out into a name private volatile int _state; /// Whether this has been disposed. private bool _disposed; - /// TimerQueueTimer used by CancelAfter and Timer-related ctors. Used instead of Timer to avoid extra allocations and because the rooted behavior is desired. - private volatile TimerQueueTimer? _timer; + /// ITimer used by CancelAfter and Timer-related ctors. Used instead of Timer to avoid extra allocations and because the rooted behavior is desired. + private volatile ITimer? _timer; /// lazily initialized and returned from . private volatile ManualResetEvent? _kernelEvent; /// Registration state for the source. @@ -137,15 +137,31 @@ public CancellationTokenSource() { } /// canceled already. /// /// - public CancellationTokenSource(TimeSpan delay) + public CancellationTokenSource(TimeSpan delay) : this(delay, TimeProvider.System) { + } + + /// Initializes a new instance of the class that will be canceled after the specified . + /// The time interval to wait before canceling this . + /// The with which to interpret the . + /// 's is less than -1 or greater than - 1. + /// is null. + /// + /// The countdown for the delay starts during the call to the constructor. When the delay expires, + /// the constructed is canceled, if it has + /// not been canceled already. Subsequent calls to CancelAfter will reset the delay for the constructed + /// , if it has not been canceled already. + /// + 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); } /// @@ -174,14 +190,14 @@ public CancellationTokenSource(int millisecondsDelay) ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.millisecondsDelay); } - InitializeWithTimer((uint)millisecondsDelay); + InitializeWithTimer((uint)millisecondsDelay, TimeProvider.System); } /// /// Common initialization logic when constructing a CTS with a delay parameter. /// A zero delay will result in immediate cancellation. /// - private void InitializeWithTimer(uint millisecondsDelay) + private void InitializeWithTimer(uint millisecondsDelay, TimeProvider timeProvider) { if (millisecondsDelay == 0) { @@ -189,8 +205,17 @@ private void InitializeWithTimer(uint millisecondsDelay) } 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(() => ...); @@ -404,7 +429,7 @@ private void CancelAfter(uint millisecondsDelay) // 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. @@ -412,16 +437,16 @@ private void CancelAfter(uint millisecondsDelay) // 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); } /// @@ -455,9 +480,8 @@ public bool TryReset() // 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) { @@ -509,11 +533,11 @@ protected virtual void Dispose(bool disposing) // 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 @@ -676,12 +700,12 @@ private bool TransitionToCancellationRequested() 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 diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/PeriodicTimer.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/PeriodicTimer.cs index 27bba5ebfca30..b784d11935bec 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/PeriodicTimer.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/PeriodicTimer.cs @@ -18,9 +18,11 @@ namespace System.Threading public sealed class PeriodicTimer : IDisposable { /// The underlying timer. - private readonly TimerQueueTimer _timer; + private readonly ITimer _timer; /// All state other than the _timer, so that the rooted timer's callback doesn't indirectly root itself by referring to _timer. private readonly State _state; + /// The timer's current period. + private TimeSpan _period; /// Initializes the timer. /// The period between ticks @@ -33,10 +35,48 @@ public PeriodicTimer(TimeSpan period) throw new ArgumentOutOfRangeException(nameof(period)); } + _period = period; _state = new State(); + _timer = new TimerQueueTimer(s => ((State)s!).Signal(), _state, ms, ms, flowExecutionContext: false); } + /// Initializes the timer. + /// The period between ticks + /// The used to interpret . + /// must be or represent a number of milliseconds equal to or larger than 1 and smaller than . + /// is null + 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); + } + } + } + /// Gets or sets the period between ticks. /// must be or represent a number of milliseconds equal to or larger than 1 and smaller than . /// @@ -46,15 +86,19 @@ public PeriodicTimer(TimeSpan period) /// 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); + } } } @@ -98,7 +142,7 @@ public ValueTask WaitForNextTickAsync(CancellationToken cancellationToken public void Dispose() { GC.SuppressFinalize(this); - _timer.Close(); + _timer.Dispose(); _state.Signal(stopping: true); } diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Future.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Future.cs index a23260dfe9844..f893e8f5332b5 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Future.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Future.cs @@ -538,22 +538,47 @@ internal override void InnerInvoke() /// The to monitor for a cancellation request. /// The representing the asynchronous wait. It may or may not be the same instance as the current instance. public new Task WaitAsync(CancellationToken cancellationToken) => - WaitAsync(Timeout.UnsignedInfinite, cancellationToken); + WaitAsync(Timeout.UnsignedInfinite, TimeProvider.System, cancellationToken); /// Gets a that will complete when this completes or when the specified timeout expires. /// The timeout after which the should be faulted with a if it hasn't otherwise completed. /// The representing the asynchronous wait. It may or may not be the same instance as the current instance. public new Task WaitAsync(TimeSpan timeout) => - WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), default); + WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), TimeProvider.System, default); + + /// + /// Gets a that will complete when this completes or when the specified timeout expires. + /// + /// The timeout after which the should be faulted with a if it hasn't otherwise completed. + /// The with which to interpret . + /// The representing the asynchronous wait. It may or may not be the same instance as the current instance. + public new Task WaitAsync(TimeSpan timeout, TimeProvider timeProvider) + { + ArgumentNullException.ThrowIfNull(timeProvider); + return WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), timeProvider, default); + } /// Gets a that will complete when this completes, when the specified timeout expires, or when the specified has cancellation requested. /// The timeout after which the should be faulted with a if it hasn't otherwise completed. /// The to monitor for a cancellation request. /// The representing the asynchronous wait. It may or may not be the same instance as the current instance. public new Task WaitAsync(TimeSpan timeout, CancellationToken cancellationToken) => - WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), cancellationToken); + WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), TimeProvider.System, cancellationToken); + + /// + /// Gets a that will complete when this completes, when the specified timeout expires, or when the specified has cancellation requested. + /// + /// The timeout after which the should be faulted with a if it hasn't otherwise completed. + /// The with which to interpret . + /// The to monitor for a cancellation request. + /// The representing the asynchronous wait. It may or may not be the same instance as the current instance. + public new Task WaitAsync(TimeSpan timeout, TimeProvider timeProvider, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(timeProvider); + return WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), timeProvider, cancellationToken); + } - private Task WaitAsync(uint millisecondsTimeout, CancellationToken cancellationToken) + private Task WaitAsync(uint millisecondsTimeout, TimeProvider timeProvider, CancellationToken cancellationToken) { if (IsCompleted || (!cancellationToken.CanBeCanceled && millisecondsTimeout == Timeout.UnsignedInfinite)) { @@ -570,7 +595,7 @@ private Task WaitAsync(uint millisecondsTimeout, CancellationToken canc return FromException(new TimeoutException()); } - return new CancellationPromise(this, millisecondsTimeout, cancellationToken); + return new CancellationPromise(this, millisecondsTimeout, timeProvider, cancellationToken); } #endregion diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs index a306088726e9c..0ed3f5c72cd00 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @@ -2781,21 +2781,44 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken) /// Gets a that will complete when this completes or when the specified has cancellation requested. /// The to monitor for a cancellation request. /// The representing the asynchronous wait. It may or may not be the same instance as the current instance. - public Task WaitAsync(CancellationToken cancellationToken) => WaitAsync(Timeout.UnsignedInfinite, cancellationToken); + public Task WaitAsync(CancellationToken cancellationToken) => WaitAsync(Timeout.UnsignedInfinite, TimeProvider.System, cancellationToken); /// Gets a that will complete when this completes or when the specified timeout expires. /// The timeout after which the should be faulted with a if it hasn't otherwise completed. /// The representing the asynchronous wait. It may or may not be the same instance as the current instance. - public Task WaitAsync(TimeSpan timeout) => WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), default); + public Task WaitAsync(TimeSpan timeout) => WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), TimeProvider.System, default); + + /// Gets a that will complete when this completes or when the specified timeout expires. + /// The timeout after which the should be faulted with a if it hasn't otherwise completed. + /// The with which to interpret . + /// The representing the asynchronous wait. It may or may not be the same instance as the current instance. + /// The argument is null. + public Task WaitAsync(TimeSpan timeout, TimeProvider timeProvider) + { + ArgumentNullException.ThrowIfNull(timeProvider); + return WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), timeProvider, default); + } /// Gets a that will complete when this completes, when the specified timeout expires, or when the specified has cancellation requested. /// The timeout after which the should be faulted with a if it hasn't otherwise completed. /// The to monitor for a cancellation request. /// The representing the asynchronous wait. It may or may not be the same instance as the current instance. 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) + /// Gets a that will complete when this completes, when the specified timeout expires, or when the specified has cancellation requested. + /// The timeout after which the should be faulted with a if it hasn't otherwise completed. + /// The with which to interpret . + /// The to monitor for a cancellation request. + /// The representing the asynchronous wait. It may or may not be the same instance as the current instance. + /// The argument is null. + 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)) { @@ -2812,7 +2835,7 @@ private Task WaitAsync(uint millisecondsTimeout, CancellationToken cancellationT return FromException(new TimeoutException()); } - return new CancellationPromise(this, millisecondsTimeout, cancellationToken); + return new CancellationPromise(this, millisecondsTimeout, timeProvider, cancellationToken); } /// Task that's completed when another task, timeout, or cancellation token triggers. @@ -2823,9 +2846,9 @@ private protected sealed class CancellationPromise : Task, ITa /// Cancellation registration used to unregister from the token source upon timeout or the task completing. private readonly CancellationTokenRegistration _registration; /// 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. - 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); @@ -2837,14 +2860,26 @@ internal CancellationPromise(Task source, uint millisecondsDelay, CancellationTo // Register with a timer if it's needed. if (millisecondsDelay != Timeout.UnsignedInfinite) { - _timer = new TimerQueueTimer(static state => + TimerCallback callback = static state => { var thisRef = (CancellationPromise)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. @@ -2888,7 +2923,7 @@ void ITaskCompletionAction.Invoke(Task completingTask) private void Cleanup() { _registration.Dispose(); - _timer?.Close(); + _timer?.Dispose(); _task.RemoveContinuation(this); } } @@ -5519,7 +5554,16 @@ public static Task Run(Func?> function, Cancella /// /// After the specified time delay, the Task is completed in RanToCompletion state. /// - public static Task Delay(TimeSpan delay) => Delay(delay, default); + public static Task Delay(TimeSpan delay) => Delay(delay, TimeProvider.System, default); + + /// Creates a task that completes after a specified time interval. + /// The to wait before completing the returned task, or to wait indefinitely. + /// The with which to interpret . + /// A task that represents the time delay. + /// represents a negative time interval other than . + /// 's property is greater than 4294967294. + /// The argument is null. + public static Task Delay(TimeSpan delay, TimeProvider timeProvider) => Delay(delay, timeProvider, default); /// /// Creates a Task that will complete after a time delay. @@ -5540,7 +5584,21 @@ public static Task Run(Func?> function, Cancella /// delay has expired. /// public static Task Delay(TimeSpan delay, CancellationToken cancellationToken) => - Delay(ValidateTimeout(delay, ExceptionArgument.delay), cancellationToken); + Delay(delay, TimeProvider.System, cancellationToken); + + /// Creates a cancellable task that completes after a specified time interval. + /// The to wait before completing the returned task, or to wait indefinitely. + /// The with which to interpret . + /// A cancellation token to observe while waiting for the task to complete. + /// A task that represents the time delay. + /// represents a negative time interval other than . + /// 's property is greater than 4294967294. + /// The argument is null. + public static Task Delay(TimeSpan delay, TimeProvider timeProvider, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(timeProvider); + return Delay(ValidateTimeout(delay, ExceptionArgument.delay), timeProvider, cancellationToken); + } /// /// Creates a Task that will complete after a time delay. @@ -5581,14 +5639,14 @@ public static Task Delay(int millisecondsDelay, CancellationToken cancellationTo 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) { @@ -5605,9 +5663,9 @@ internal static uint ValidateTimeout(TimeSpan timeout, ExceptionArgument argumen 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); @@ -5619,13 +5677,24 @@ internal DelayPromise(uint millisecondsDelay) 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(); } } } @@ -5647,7 +5716,7 @@ private void CompleteTimedOut() } } - protected virtual void Cleanup() => _timer?.Close(); + protected virtual void Cleanup() => _timer?.Dispose(); } /// DelayPromise that also supports cancellation. @@ -5655,7 +5724,7 @@ private sealed class DelayPromiseWithCancellation : DelayPromise { 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); diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs index 40ecd77c61b41..5cd51ae4086fa 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Tracing; +using System.Runtime.CompilerServices; using System.Threading.Tasks; namespace System.Threading @@ -444,7 +445,7 @@ public void DeleteTimer(TimerQueueTimer timer) // 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; @@ -484,6 +485,19 @@ internal sealed class TimerQueueTimer : IThreadPoolWorkItem 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; @@ -519,7 +533,10 @@ internal string DisplayString } } - 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; @@ -527,7 +544,6 @@ internal bool Change(uint dueTime, uint period, bool throwIfDisposed = true) { if (_canceled) { - ObjectDisposedException.ThrowIf(throwIfDisposed, this); return false; } @@ -549,8 +565,7 @@ internal bool Change(uint dueTime, uint period, bool throwIfDisposed = true) return success; } - - public void Close() + public void Dispose() { lock (_associatedTimerQueue) { @@ -562,8 +577,7 @@ public void Close() } } - - public bool Close(WaitHandle toSignal) + public bool Dispose(WaitHandle toSignal) { Debug.Assert(toSignal != null); @@ -592,7 +606,7 @@ public bool Close(WaitHandle toSignal) return success; } - public ValueTask CloseAsync() + public ValueTask DisposeAsync() { lock (_associatedTimerQueue) { @@ -779,25 +793,25 @@ public TimerHolder(TimerQueueTimer timer) ~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; } @@ -805,7 +819,7 @@ public ValueTask CloseAsync() [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; @@ -899,10 +913,8 @@ public bool Change(int dueTime, int period) 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) @@ -944,17 +956,17 @@ public bool Dispose(WaitHandle notifyObject) { 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; diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 53f5e047b232a..2a8e2960ab786 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -1849,6 +1849,19 @@ public enum DayOfWeek 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() { } @@ -14459,6 +14472,7 @@ public void Dispose() { } 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; } } @@ -14484,6 +14498,7 @@ public enum LazyThreadSafetyMode 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 { } } @@ -14494,7 +14509,11 @@ public static partial class Timeout 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) { } @@ -14613,6 +14632,8 @@ public Task(System.Action action, object? state, System.Threading.Tasks 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; } @@ -14657,6 +14678,8 @@ public static void WaitAll(System.Threading.Tasks.Task[] tasks, System.Threading 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 tasks) { throw null; } public static System.Threading.Tasks.Task WhenAll(params System.Threading.Tasks.Task[] tasks) { throw null; } public static System.Threading.Tasks.Task WhenAll(System.Collections.Generic.IEnumerable> tasks) { throw null; } @@ -14958,6 +14981,8 @@ public Task(System.Func function, System.Threading.Tasks.TaskCreationOp public new System.Threading.Tasks.Task WaitAsync(System.Threading.CancellationToken cancellationToken) { throw null; } public new System.Threading.Tasks.Task WaitAsync(System.TimeSpan timeout) { throw null; } public new System.Threading.Tasks.Task WaitAsync(System.TimeSpan timeout, System.Threading.CancellationToken cancellationToken) { throw null; } + public new System.Threading.Tasks.Task WaitAsync(System.TimeSpan timeout, System.TimeProvider timeProvider) { throw null; } + public new System.Threading.Tasks.Task WaitAsync(System.TimeSpan timeout, System.TimeProvider timeProvider, System.Threading.CancellationToken cancellationToken) { throw null; } } public static partial class TaskToAsyncResult { diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj b/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj index 91dbd5cc20545..0b474b78b91c6 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj @@ -10,12 +10,12 @@ true - + true - @@ -41,6 +41,7 @@ + @@ -339,7 +340,7 @@ - + diff --git a/src/libraries/System.Threading.Timer/tests/TimerChangeTests.cs b/src/libraries/System.Threading.Timer/tests/TimerChangeTests.cs index 6de015852a8d0..fdd62265a1680 100644 --- a/src/libraries/System.Threading.Timer/tests/TimerChangeTests.cs +++ b/src/libraries/System.Threading.Timer/tests/TimerChangeTests.cs @@ -38,14 +38,15 @@ public void Timer_Change_Period_OutOfRange_Throws() } [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(() => t.Change(1, 1)); - Assert.Throws(() => t.Change(1L, 1L)); - Assert.Throws(() => t.Change(1u, 1u)); - Assert.Throws(() => 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))] diff --git a/src/libraries/System.Threading.Timer/tests/TimerDisposeTests.cs b/src/libraries/System.Threading.Timer/tests/TimerDisposeTests.cs index 4159f97c6928d..81cf68b4f2c6d 100644 --- a/src/libraries/System.Threading.Timer/tests/TimerDisposeTests.cs +++ b/src/libraries/System.Threading.Timer/tests/TimerDisposeTests.cs @@ -28,7 +28,7 @@ public async Task DisposeAsync_DisposesTimer() { var t = new Timer(_ => { }); await t.DisposeAsync(); - Assert.Throws(() => t.Change(-1, -1)); + Assert.False(t.Change(-1, -1)); } [Fact] diff --git a/src/libraries/System.Threading.Timer/tests/TimerFiringTests.cs b/src/libraries/System.Threading.Timer/tests/TimerFiringTests.cs index c01c8b4e64419..cf6b97e0be20f 100644 --- a/src/libraries/System.Threading.Timer/tests/TimerFiringTests.cs +++ b/src/libraries/System.Threading.Timer/tests/TimerFiringTests.cs @@ -236,7 +236,7 @@ public void Timer_Dispose_WaitHandle() t.Dispose(allTicksCompleted); Assert.True(allTicksCompleted.WaitOne(MaxPositiveTimeoutInMs)); Assert.Equal(0, tickCount); - Assert.Throws(() => t.Change(0, 0)); + Assert.False(t.Change(0, 0)); } [OuterLoop("Incurs seconds delay to wait for events that should never happen")]