Tickers in C#

Timers are for when you want to do something once in the future - tickers are for when you want to do something repeatedly at regular intervals. Here’s an example of a ticker that ticks periodically until we stop it.

using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        // Tickers use a similar mechanism to timers: a Timer object that raises an event at regular intervals.
        // Here we'll use a Timer and a TaskCompletionSource to simulate the behavior of Go's ticker.
        var timer = new Timer(Callback, null, 0, 500);
        var tcs = new TaskCompletionSource<bool>();

        async Task TickerLoop()
        {
            while (true)
            {
                if (await Task.WhenAny(tcs.Task, Task.Delay(500)) == tcs.Task)
                {
                    return;
                }
                Console.WriteLine($"Tick at {DateTime.Now}");
            }
        }

        // Start the ticker loop
        _ = TickerLoop();

        // Tickers can be stopped. Once a ticker is stopped it won't receive any more values.
        // We'll stop ours after 1600ms.
        await Task.Delay(1600);
        timer.Dispose();
        tcs.SetResult(true);
        Console.WriteLine("Ticker stopped");
    }

    static void Callback(object state)
    {
        // This method is empty because we're using the Timer just to trigger the loop in TickerLoop
    }
}

When we run this program the ticker should tick 3 times before we stop it.

$ dotnet run
Tick at 5/30/2023 10:15:30 AM
Tick at 5/30/2023 10:15:30 AM
Tick at 5/30/2023 10:15:31 AM
Ticker stopped

In this C# version, we use a Timer object to simulate the behavior of Go’s ticker. The TickerLoop method runs continuously, printing the current time every 500 milliseconds until it’s signaled to stop via the TaskCompletionSource.

The main difference from the Go version is that C# doesn’t have built-in channels, so we use a combination of Timer, Task, and TaskCompletionSource to achieve similar functionality. The Task.WhenAny method is used to either wait for the next tick or for the cancellation signal, whichever comes first.

After 1600 milliseconds, we dispose of the timer and signal the loop to stop, similar to the Go version.