Skip to content

Commit 5d88ff4

Browse files
authored
Introducing Time abstraction Part1 (#83604)
1 parent 2d2528b commit 5d88ff4

File tree

15 files changed

+953
-95
lines changed

15 files changed

+953
-95
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace System.Threading
5+
{
6+
/// <summary>Represents a timer that can have its due time and period changed.</summary>
7+
/// <remarks>
8+
/// Implementations of <see cref="Change"/>, <see cref="IDisposable.Dispose"/>, and <see cref="IAsyncDisposable.DisposeAsync"/>
9+
/// must all be thread-safe such that the timer instance may be accessed concurrently from multiple threads.
10+
/// </remarks>
11+
public interface ITimer : IDisposable, IAsyncDisposable
12+
{
13+
/// <summary>Changes the start time and the interval between method invocations for a timer, using <see cref="TimeSpan"/> values to measure time intervals.</summary>
14+
/// <param name="dueTime">
15+
/// A <see cref="TimeSpan"/> representing the amount of time to delay before invoking the callback method specified when the <see cref="ITimer"/> was constructed.
16+
/// Specify <see cref="Timeout.InfiniteTimeSpan"/> to prevent the timer from restarting. Specify <see cref="TimeSpan.Zero"/> to restart the timer immediately.
17+
/// </param>
18+
/// <param name="period">
19+
/// The time interval between invocations of the callback method specified when the Timer was constructed.
20+
/// Specify <see cref="Timeout.InfiniteTimeSpan"/> to disable periodic signaling.
21+
/// </param>
22+
/// <returns><see langword="true"/> if the timer was successfully updated; otherwise, <see langword="false"/>.</returns>
23+
/// <exception cref="ArgumentOutOfRangeException">The <paramref name="dueTime"/> or <paramref name="period"/> parameter, in milliseconds, is less than -1 or greater than 4294967294.</exception>
24+
/// <remarks>
25+
/// It is the responsibility of the implementer of the ITimer interface to ensure thread safety.
26+
/// </remarks>
27+
bool Change(TimeSpan dueTime, TimeSpan period);
28+
}
29+
}
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
8+
namespace System
9+
{
10+
/// <summary>Provides an abstraction for time.</summary>
11+
public abstract class TimeProvider
12+
{
13+
private readonly double _timeToTicksRatio;
14+
15+
/// <summary>
16+
/// Gets a <see cref="TimeProvider"/> that provides a clock based on <see cref="DateTimeOffset.UtcNow"/>,
17+
/// a time zone based on <see cref="TimeZoneInfo.Local"/>, a high-performance time stamp based on <see cref="Stopwatch"/>,
18+
/// and a timer based on <see cref="Timer"/>.
19+
/// </summary>
20+
/// <remarks>
21+
/// 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"/>.
22+
/// </remarks>
23+
public static TimeProvider System { get; } = new SystemTimeProvider(null);
24+
25+
/// <summary>
26+
/// Initializes the instance with the timestamp frequency.
27+
/// </summary>
28+
/// <exception cref="ArgumentOutOfRangeException">The value of <paramref name="timestampFrequency"/> is negative or zero.</exception>
29+
/// <param name="timestampFrequency">Frequency of the values returned from <see cref="GetTimestamp"/> method.</param>
30+
protected TimeProvider(long timestampFrequency)
31+
{
32+
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(timestampFrequency);
33+
TimestampFrequency = timestampFrequency;
34+
_timeToTicksRatio = (double)TimeSpan.TicksPerSecond / TimestampFrequency;
35+
}
36+
37+
/// <summary>
38+
/// Gets a <see cref="DateTimeOffset"/> value whose date and time are set to the current
39+
/// Coordinated Universal Time (UTC) date and time and whose offset is Zero,
40+
/// all according to this <see cref="TimeProvider"/>'s notion of time.
41+
/// </summary>
42+
public abstract DateTimeOffset UtcNow { get; }
43+
44+
/// <summary>
45+
/// Gets a <see cref="DateTimeOffset"/> value that is set to the current date and time according to this <see cref="TimeProvider"/>'s
46+
/// notion of time based on <see cref="UtcNow"/>, with the offset set to the <see cref="LocalTimeZone"/>'s offset from Coordinated Universal Time (UTC).
47+
/// </summary>
48+
public DateTimeOffset LocalNow
49+
{
50+
get
51+
{
52+
DateTime utcDateTime = UtcNow.UtcDateTime;
53+
TimeSpan offset = LocalTimeZone.GetUtcOffset(utcDateTime);
54+
55+
long localTicks = utcDateTime.Ticks + offset.Ticks;
56+
if ((ulong)localTicks > DateTime.MaxTicks)
57+
{
58+
localTicks = localTicks < DateTime.MinTicks ? DateTime.MinTicks : DateTime.MaxTicks;
59+
}
60+
61+
return new DateTimeOffset(localTicks, offset);
62+
}
63+
}
64+
65+
/// <summary>
66+
/// Gets a <see cref="TimeZoneInfo"/> object that represents the local time zone according to this <see cref="TimeProvider"/>'s notion of time.
67+
/// </summary>
68+
public abstract TimeZoneInfo LocalTimeZone { get; }
69+
70+
/// <summary>
71+
/// Gets the frequency of <see cref="GetTimestamp"/> of high-frequency value per second.
72+
/// </summary>
73+
public long TimestampFrequency { get; }
74+
75+
/// <summary>
76+
/// Creates a <see cref="TimeProvider"/> that provides a clock based on <see cref="DateTimeOffset.UtcNow"/>,
77+
/// a time zone based on <paramref name="timeZone"/>, a high-performance time stamp based on <see cref="Stopwatch"/>,
78+
/// and a timer based on <see cref="Timer"/>.
79+
/// </summary>
80+
/// <param name="timeZone">The time zone to use in getting the local time using <see cref="LocalNow"/>. </param>
81+
/// <returns>A new instance of <see cref="TimeProvider"/>. </returns>
82+
/// <exception cref="ArgumentNullException"><paramref name="timeZone"/> is null.</exception>
83+
public static TimeProvider FromLocalTimeZone(TimeZoneInfo timeZone)
84+
{
85+
ArgumentNullException.ThrowIfNull(timeZone);
86+
return new SystemTimeProvider(timeZone);
87+
}
88+
89+
/// <summary>
90+
/// Gets the current high-frequency value designed to measure small time intervals with high accuracy in the timer mechanism.
91+
/// </summary>
92+
/// <returns>A long integer representing the high-frequency counter value of the underlying timer mechanism. </returns>
93+
public abstract long GetTimestamp();
94+
95+
/// <summary>
96+
/// Gets the elapsed time between two timestamps retrieved using <see cref="GetTimestamp"/>.
97+
/// </summary>
98+
/// <param name="startingTimestamp">The timestamp marking the beginning of the time period.</param>
99+
/// <param name="endingTimestamp">The timestamp marking the end of the time period.</param>
100+
/// <returns>A <see cref="TimeSpan"/> for the elapsed time between the starting and ending timestamps.</returns>
101+
public TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) =>
102+
new TimeSpan((long)((endingTimestamp - startingTimestamp) * _timeToTicksRatio));
103+
104+
/// <summary>Creates a new <see cref="ITimer"/> instance, using <see cref="TimeSpan"/> values to measure time intervals.</summary>
105+
/// <param name="callback">
106+
/// A delegate representing a method to be executed when the timer fires. The method specified for callback should be reentrant,
107+
/// as it may be invoked simultaneously on two threads if the timer fires again before or while a previous callback is still being handled.
108+
/// </param>
109+
/// <param name="state">An object to be passed to the <paramref name="callback"/>. This may be null.</param>
110+
/// <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>
111+
/// <param name="period">The time interval between invocations of <paramref name="callback"/>. Specify <see cref="Timeout.InfiniteTimeSpan"/> to disable periodic signaling.</param>
112+
/// <returns>
113+
/// The newly created <see cref="ITimer"/> instance.
114+
/// </returns>
115+
/// <exception cref="ArgumentNullException"><paramref name="callback"/> is null.</exception>
116+
/// <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>
117+
/// <remarks>
118+
/// <para>
119+
/// 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.
120+
/// </para>
121+
/// <para>
122+
/// 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,
123+
/// but can be re-enabled by calling the <see cref="ITimer.Change"/> method.
124+
/// </para>
125+
/// <para>
126+
/// 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,
127+
/// but can be re-enabled using the <see cref="ITimer.Change"/> method.
128+
/// </para>
129+
/// <para>
130+
/// The return <see cref="ITimer"/> instance will be implicitly rooted while the timer is still scheduled.
131+
/// </para>
132+
/// <para>
133+
/// <see cref="CreateTimer"/> captures the <see cref="ExecutionContext"/> and stores that with the <see cref="ITimer"/> for use in invoking <paramref name="callback"/>
134+
/// each time it's called. That capture can be suppressed with <see cref="ExecutionContext.SuppressFlow"/>.
135+
/// </para>
136+
/// </remarks>
137+
public abstract ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period);
138+
139+
/// <summary>
140+
/// Provides a default implementation of <see cref="TimeProvider"/> based on <see cref="DateTimeOffset.UtcNow"/>,
141+
/// <see cref="TimeZoneInfo.Local"/>, <see cref="Stopwatch"/>, and <see cref="Timer"/>.
142+
/// </summary>
143+
private sealed class SystemTimeProvider : TimeProvider
144+
{
145+
/// <summary>The time zone to treat as local. If null, <see cref="TimeZoneInfo.Local"/> is used.</summary>
146+
private readonly TimeZoneInfo? _localTimeZone;
147+
148+
/// <summary>Initializes the instance.</summary>
149+
/// <param name="localTimeZone">The time zone to treat as local. If null, <see cref="TimeZoneInfo.Local"/> is used.</param>
150+
internal SystemTimeProvider(TimeZoneInfo? localTimeZone) : base(Stopwatch.Frequency) => _localTimeZone = localTimeZone;
151+
152+
/// <inheritdoc/>
153+
public override TimeZoneInfo LocalTimeZone => _localTimeZone ?? TimeZoneInfo.Local;
154+
155+
/// <inheritdoc/>
156+
public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period)
157+
{
158+
ArgumentNullException.ThrowIfNull(callback);
159+
return new SystemTimeProviderTimer(dueTime, period, callback, state);
160+
}
161+
162+
/// <inheritdoc/>
163+
public override long GetTimestamp() => Stopwatch.GetTimestamp();
164+
165+
/// <inheritdoc/>
166+
public override DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
167+
168+
/// <summary>Thin wrapper for a <see cref="TimerQueueTimer"/>.</summary>
169+
/// <remarks>
170+
/// We don't return a TimerQueueTimer directly as it implements IThreadPoolWorkItem and we don't
171+
/// want it exposed in a way that user code could directly queue the timer to the thread pool.
172+
/// We also use this instead of Timer because CreateTimer needs to return a timer that's implicitly
173+
/// rooted while scheduled.
174+
/// </remarks>
175+
private sealed class SystemTimeProviderTimer : ITimer
176+
{
177+
private readonly TimerQueueTimer _timer;
178+
179+
public SystemTimeProviderTimer(TimeSpan dueTime, TimeSpan period, TimerCallback callback, object? state)
180+
{
181+
(uint duration, uint periodTime) = CheckAndGetValues(dueTime, period);
182+
_timer = new TimerQueueTimer(callback, state, duration, periodTime, flowExecutionContext: true);
183+
}
184+
185+
public bool Change(TimeSpan dueTime, TimeSpan period)
186+
{
187+
(uint duration, uint periodTime) = CheckAndGetValues(dueTime, period);
188+
return _timer.Change(duration, periodTime);
189+
}
190+
191+
public void Dispose() => _timer.Dispose();
192+
193+
public ValueTask DisposeAsync() => _timer.DisposeAsync();
194+
195+
private static (uint duration, uint periodTime) CheckAndGetValues(TimeSpan dueTime, TimeSpan periodTime)
196+
{
197+
long dueTm = (long)dueTime.TotalMilliseconds;
198+
ArgumentOutOfRangeException.ThrowIfLessThan(dueTm, -1, nameof(dueTime));
199+
ArgumentOutOfRangeException.ThrowIfGreaterThan(dueTm, Timer.MaxSupportedTimeout, nameof(dueTime));
200+
201+
long periodTm = (long)periodTime.TotalMilliseconds;
202+
ArgumentOutOfRangeException.ThrowIfLessThan(periodTm, -1, nameof(periodTime));
203+
ArgumentOutOfRangeException.ThrowIfGreaterThan(periodTm, Timer.MaxSupportedTimeout, nameof(periodTime));
204+
205+
return ((uint)dueTm, (uint)periodTm);
206+
}
207+
}
208+
}
209+
}
210+
}

0 commit comments

Comments
 (0)