Skip to content

API proposal: Modern Timer API #31525

Closed
@davidfowl

Description

@davidfowl

To solve of the common issues with timers:

  1. The fact that it always captures the execution context is problematic for certain long lived operations (https://github.com/dotnet/corefx/issues/32866)
  2. The fact that is has strange rooting behavior based on the constructor used
  3. The fact timer callbacks can overlap (https://github.com/dotnet/corefx/issues/39313)
  4. 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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions