Closed
Description
To solve of the common issues with timers:
- The fact that it always captures the execution context is problematic for certain long lived operations (https://github.com/dotnet/corefx/issues/32866)
- The fact that is has strange rooting behavior based on the constructor used
- The fact timer callbacks can overlap (https://github.com/dotnet/corefx/issues/39313)
- The fact that timer callbacks aren't asynchronous which leads to people writing sync over async code
I propose we create a modern timer API based that basically solves all of these problems 😄 .
- This API only makes sense for timers that fire repeatedly, timers that fire once could be Task based (we already have
Task.Delay
for this) - The timer will be paused while user code is executing and will resume the next period once it ends.
- The timer can be stopped using a CancellationToken provided to stop the enumeration.
- The execution context isn't captured.
+namespace System.Threading
+{
+ public class AsyncTimer : IDisposable, IAsyncDisposable
+ {
+ public AsyncTimer(TimeSpan period);
+ public ValueTask<bool> WaitForNextTickAsync(CancellationToken cancellationToken);
+ public void Stop();
+ }
+}
Usage:
class Program
{
static async Task Main(string[] args)
{
var second = TimeSpan.FromSeconds(1);
using var timer = new AsyncTimer(second);
while (await timer.WaitForNextTickAsync())
{
Console.WriteLine($"Tick {DateTime.Now}")
}
}
}
public class WatchDog
{
private CanceallationTokenSource _cts = new();
private Task _timerTask;
public void Start()
{
async Task DoStart()
{
try
{
await using var timer = new AsyncTimer(TimeSpan.FromSeconds(5));
while (await timer.WaitForNextTickAsync(_cts.Token))
{
await CheckPingAsync();
}
}
catch (OperationCancelledException)
{
}
}
_timerTask = DoStart();
}
public async Task StopAsync()
{
_cts.Cancel();
await _timerTask;
_cts.Dispose();
}
}
Risks
New Timer API, more choice, when to use over Task.Delay?
Alternatives
Alternative 1: IAsyncEnumerable
The issue with IAsyncEnumerable is that we don't have a non-generic version. In this case we don't need to return anything per iteration (the object here is basically void). There were also concerns raised around the fact that IAsyncEnumerable<T>
is used for returning data and not so much for an async stream of events that don't have data.
public class Timer
{
+ public IAsyncEnumerable<object> Periodic(TimeSpan period);
}
class Program
{
static async Task Main(string[] args)
{
var second = TimeSpan.FromSeconds(1);
await foreach(var _ in Timer.Periodic(second))
{
Console.WriteLine($"Tick {DateTime.Now}")
}
}
}
Alternative 2: add methods to Timer
- Avoids a new type
- Confusing to think about what happens when WaitForNextTickAsync is called when a different constructor is called.
public class Timer
{
+ public Timer(TimeSpan period);
+ ValueTask<bool> WaitForNextTickAsync(CancellationToken cancellationToken);
}
cc @stephentoub