Skip to content
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

Add System.Threading.PeriodicTimer #53899

Merged
merged 1 commit into from
Jun 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1069,6 +1069,7 @@
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\ThreadStateException.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\Timeout.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\TimeoutHelper.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\PeriodicTimer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\Timer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\TimerQueue.Portable.cs" Condition="'$(FeaturePortableTimer)' == 'true'" />
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\Volatile.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
// 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.Runtime.ExceptionServices;
using System.Threading.Tasks;
using System.Threading.Tasks.Sources;

namespace System.Threading
{
/// <summary>Provides a periodic timer that enables waiting asynchronously for timer ticks.</summary>
/// <remarks>
/// This timer is intended to be used only by a single consumer at a time: only one call to <see cref="WaitForNextTickAsync" />
/// may be in flight at any given moment. <see cref="Dispose"/> may be used concurrently with an active <see cref="WaitForNextTickAsync"/>
/// to interrupt it and cause it to return false.
/// </remarks>
public sealed class PeriodicTimer : IDisposable
{
/// <summary>The underlying timer.</summary>
private readonly TimerQueueTimer _timer;
/// <summary>All state other than the _timer, so that the rooted timer's callback doesn't indirectly root itself by referring to _timer.</summary>
private readonly State _state;

/// <summary>Initializes the timer.</summary>
/// <param name="period">The time interval between invocations of callback..</param>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="period"/> must be represent a number of milliseconds larger than 0 and smaller than <see cref="uint.MaxValue"/>.</exception>
public PeriodicTimer(TimeSpan period)
{
long ms = (long)period.TotalMilliseconds;
if (ms < 1 || ms > Timer.MaxSupportedTimeout)
{
GC.SuppressFinalize(this);
throw new ArgumentOutOfRangeException(nameof(period));
}

_state = new State();
_timer = new TimerQueueTimer(s => ((State)s!).Signal(), _state, (uint)ms, (uint)ms, flowExecutionContext: false);
stephentoub marked this conversation as resolved.
Show resolved Hide resolved
}

/// <summary>Wait for the next tick of the timer, or for the timer to be stopped.</summary>
/// <param name="cancellationToken">
/// A <see cref="CancellationToken"/> to use to cancel the asynchronous wait. If cancellation is requested, it affects only the single wait operation;
/// the underlying timer continues firing.
/// </param>
/// <returns>A task that will be completed due to the timer firing, <see cref="Dispose"/> being called to stop the timer, or cancellation being requested.</returns>
/// <remarks>
/// The <see cref="PeriodicTimer"/> behaves like an auto-reset event, in that multiple ticks are coalesced into a single tick if they occur between
/// calls to <see cref="WaitForNextTickAsync"/>. Similarly, a call to <see cref="Dispose"/> will void any tick not yet consumed. <see cref="WaitForNextTickAsync"/>
/// may only be used by one consumer at a time, and may be used concurrently with a single call to <see cref="Dispose"/>.
/// </remarks>
public ValueTask<bool> WaitForNextTickAsync(CancellationToken cancellationToken = default) =>
_state.WaitForNextTickAsync(this, cancellationToken);

/// <summary>Stops the timer and releases associated managed resources.</summary>
/// <remarks>
/// <see cref="Dispose"/> will cause an active wait with <see cref="WaitForNextTickAsync"/> to complete with a value of false.
/// All subsequent <see cref="WaitForNextTickAsync"/> invocations will produce a value of false.
/// </remarks>
public void Dispose()
{
GC.SuppressFinalize(this);
_timer.Close();
_state.Signal(stopping: true);
}

~PeriodicTimer() => Dispose();

/// <summary>Core implementation for the periodic timer.</summary>
private sealed class State : IValueTaskSource<bool>
{
/// <summary>The associated <see cref="PeriodicTimer"/>.</summary>
/// <remarks>
/// This should refer to the parent instance only when there's an active waiter, and be null when there
/// isn't. The TimerQueueTimer in the PeriodicTimer strongly roots itself, and it references this State
/// object:
/// PeriodicTimer (finalizable) --ref--> TimerQueueTimer (rooted) --ref--> State --ref--> null
/// If this State object then references the PeriodicTimer, it creates a strongly-rooted cycle that prevents anything from
/// being GC'd:
/// PeriodicTimer (finalizable) --ref--> TimerQueueTimer (rooted) --ref--> State --v
/// ^--ref-------------------------------------------------------------------|
/// When this field is null, the cycle is broken, and dropping all references to the PeriodicTimer allows the
/// PeriodicTimer to be finalized and unroot the TimerQueueTimer. Thus, we keep this field set during<see cref="WaitForNextTickAsync"/>
/// so that the timer roots any async continuation chain awaiting it, and then keep it unset otherwise so that everything
/// can be GC'd appropriately.
/// </remarks>
private PeriodicTimer? _owner;
/// <summary>Core of the <see cref="IValueTaskSource{TResult}"/> implementation.</summary>
private ManualResetValueTaskSourceCore<bool> _mrvtsc;
/// <summary>Cancellation registration for any active <see cref="WaitForNextTickAsync"/> call.</summary>
private CancellationTokenRegistration _ctr;
/// <summary>Whether the timer has been stopped.</summary>
private bool _stopped;
/// <summary>Whether there's a pending notification to be received. This could be due to the timer firing, the timer being stopped, or cancellation being requested.</summary>
private bool _signaled;
/// <summary>Whether there's a <see cref="WaitForNextTickAsync"/> call in flight.</summary>
private bool _activeWait;

/// <summary>Wait for the next tick of the timer, or for the timer to be stopped.</summary>
public ValueTask<bool> WaitForNextTickAsync(PeriodicTimer owner, CancellationToken cancellationToken)
{
lock (this)
{
if (_activeWait)
{
// WaitForNextTickAsync should only be used by one consumer at a time. Failing to do so is an error.
ThrowHelper.ThrowInvalidOperationException();
}

// If cancellation has already been requested, short-circuit.
if (cancellationToken.IsCancellationRequested)
{
return ValueTask.FromCanceled<bool>(cancellationToken);
}

// If the timer has a pending tick or has been stopped, we can complete synchronously.
if (_signaled)
{
// Reset the signal for subsequent consumers, but only if we're not stopped. Since.
// stopping the timer is one way, any subsequent calls should also complete synchronously
// with false, and thus we leave _signaled pinned at true.
if (!_stopped)
{
_signaled = false;
}

return new ValueTask<bool>(!_stopped);
}

Debug.Assert(!_stopped, "Unexpectedly stopped without _signaled being true.");

// Set up for the wait and return a task that will be signaled when the
// timer fires, stop is called, or cancellation is requested.
_owner = owner;
_activeWait = true;
_ctr = cancellationToken.UnsafeRegister(static (state, cancellationToken) => ((State)state!).Signal(cancellationToken: cancellationToken), this);
stephentoub marked this conversation as resolved.
Show resolved Hide resolved

return new ValueTask<bool>(this, _mrvtsc.Version);
}
}

/// <summary>Signal that the timer has either fired or been stopped.</summary>
public void Signal(bool stopping = false, CancellationToken cancellationToken = default)
{
bool completeTask = false;

lock (this)
{
_stopped |= stopping;
if (!_signaled)
{
_signaled = true;
completeTask = _activeWait;
}
}

if (completeTask)
{
if (cancellationToken.IsCancellationRequested)
{
// If cancellation is requested just before the UnsafeRegister call, it's possible this will end up being invoked
// as part of the WaitForNextTickAsync call and thus as part of holding the lock. The goal of completeTask
// was to escape that lock, so that we don't invoke any synchronous continuations from the ValueTask as part
// of completing _mrvtsc. However, in that case, we also haven't returned the ValueTask to the caller, so there
// won't be any continuations yet, which makes this safe.
_mrvtsc.SetException(ExceptionDispatchInfo.SetCurrentStackTrace(new OperationCanceledException(cancellationToken)));
}
else
{
Debug.Assert(!Monitor.IsEntered(this));
_mrvtsc.SetResult(true);
}
}
}

/// <inheritdoc/>
bool IValueTaskSource<bool>.GetResult(short token)
{
// Dispose of the cancellation registration. This is done outside of the below lock in order
// to avoid a potential deadlock due to waiting for a concurrent cancellation callback that might
// in turn try to take the lock. For valid usage, GetResult is only called once _ctr has been
// successfully initialized before WaitForNextTickAsync returns to its synchronous caller, and
// there should be no race conditions accessing it, as concurrent consumption is invalid. If there
// is invalid usage, with GetResult used erroneously/concurrently, the worst that happens is cancellation
// may not take effect for the in-flight operation, with its registration erroneously disposed.
// Note we use Dispose rather than Unregister (which wouldn't risk deadlock) so that we know that thecancellation callback associated with this operation
// won't potentially still fire after we've completed this GetResult and a new operation
// has potentially started.
_ctr.Dispose();

lock (this)
{
try
{
_mrvtsc.GetResult(token);
}
finally
{
_mrvtsc.Reset();
_ctr = default;
_activeWait = false;
_owner = null;
if (!_stopped)
{
_signaled = false;
}
}

return !_stopped;
}
}

/// <inheritdoc/>
ValueTaskSourceStatus IValueTaskSource<bool>.GetStatus(short token) => _mrvtsc.GetStatus(token);

/// <inheritdoc/>
void IValueTaskSource<bool>.OnCompleted(Action<object?> continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) =>
_mrvtsc.OnCompleted(continuation, state, token, flags);
}
}
}
6 changes: 6 additions & 0 deletions src/libraries/System.Runtime/ref/System.Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11651,6 +11651,12 @@ public enum LazyThreadSafetyMode
PublicationOnly = 1,
ExecutionAndPublication = 2,
}
public sealed class PeriodicTimer : System.IDisposable
{
public PeriodicTimer(System.TimeSpan period) { }
public System.Threading.Tasks.ValueTask<bool> WaitForNextTickAsync(System.Threading.CancellationToken cancellationToken = default) { throw null; }
public void Dispose() { }
}
public static partial class Timeout
{
public const int Infinite = -1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@
<Compile Include="System\Security\SecurityAttributeTests.cs" />
<Compile Include="System\Security\SecurityExceptionTests.cs" />
<Compile Include="System\Text\StringBuilderTests.cs" />
<Compile Include="System\Threading\PeriodicTimerTests.cs" />
<Compile Include="System\Threading\WaitHandleTests.cs" />
<Compile Include="System\Type\TypePropertyTests.cs" />
<Compile Include="System\Type\TypeTests.cs" />
Expand Down
Loading