-
Notifications
You must be signed in to change notification settings - Fork 5.1k
Introducing Time abstraction Part1 #83604
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a30d962
7d773ed
f4f5d6a
92dcf0f
e324eed
ee49118
a4be8ed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
{ | ||
/// <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); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
{ | ||
/// <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"/>, | ||
tarekgh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// 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); | ||
tarekgh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
/// <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> | ||
tarekgh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// <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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to consider some kind of out parameter that would allow this to communicate back whether all work has quiesced? Or are you thinking we'll investigate that as a follow-up? This is to allow, for example, CancellationTokenSource.TryReset to work with other timer implementations. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's have a follow up on that to see if there are any more things we need to consider. I'll create an issue for that. |
||
|
||
/// <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) | ||
stephentoub marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
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); | ||
} | ||
} | ||
} | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.