DEV Community

John Smith
John Smith

Posted on • Originally published at solrevdev.com on

2 1

Timers in .NET

A current C# project of mine required a timer where every couple of seconds a method would fire and a potentially fairly long-running process would run.

With .NET we have a few built-in options for timers:

System.Web.UI.Timer

Available in the .NET Framework 4.8 which performs asynchronous or synchronous Web page postbacks at a defined interval and was used back in the older WebForms days.

System.Windows.Forms.Timer

This timer is optimized for use in Windows Forms applications and must be used in a window.

System.Timers.Timer

Generates an event after a set interval, with an option to generate recurring events. This timer is almost what I need however this has quite a few stackoverflow posts where exceptions get swallowed.

System.Threading.Timer

Provides a mechanism for executing a method on a thread pool thread at specified intervals and is the one I decided to go with.

Issues

I came across a couple of minor issues the first being that even though I held a reference to my Timer object in my class and disposed of it in a Dispose method the timer would stop ticking after a while suggesting that the garbage collector was sweeping up and removing it.

My Dispose method looks like the first method below and I suspect it is because I am using the conditional access shortcut feature from C# 6 rather than explicitly checking for null first.

public void Dispose()
{ 
    // conditional access shortcut
    _timer?.Dispose(); 
} 

public void Dispose()
{ 
    // null check
    if(_timer != null)
    {
        _timer.Dispose(); 
    }
} 

A workaround is to tell the garbage collector to not collect this reference by using this line of code in timer’s elapsed method.

GC.KeepAlive(_timer);

The next issue was that my TimerTick event would fire and before the method that was being called could finish another tick event would fire.

This required a stackoverflow search where the following code fixed my issue.

// private field
private readonly object _locker = new object();

// this in TimerTick event
if (Monitor.TryEnter(_locker))
{
    try
    {
        // do long running work here
        DoWork();
    }
    finally
    {
        Monitor.Exit(_locker);
    }
}

And so with these two fixes in place, my timer work was behaving as expected.

Solution

Here is a sample class with the above code all in context for future reference

using System;
using System.Threading;
public class TimerTest : IDisposable
{
private readonly Timer _timer;
private readonly object _locker = new object();
public TimerTest()
{
_timer = new Timer(new TimerCallback(TickTimer), null, 1000, 1000);
}
private void TickTimer(object state)
{
// keep GC from collecting and stopping timer;
GC.KeepAlive(_timer);
// do not let another tick happen if we are still doing things - https://stackoverflow.com/a/13267259/2041
if (Monitor.TryEnter(_locker))
{
try
{
// do long running work here
DoWork();
}
finally
{
Monitor.Exit(_locker);
}
}
}
private void DoWork()
{
Console.WriteLine("Starting DoWork()");
Thread.Sleep(2000);
Console.WriteLine("Ending DoWork()");
}
public void Dispose()
{
_timer?.Dispose();
}
}
view raw TimerTest.cs hosted with ❤ by GitHub

Success 🎉

Heroku

Simplify your DevOps and maximize your time.

Since 2007, Heroku has been the go-to platform for developers as it monitors uptime, performance, and infrastructure concerns, allowing you to focus on writing code.

Learn More

Top comments (5)

Collapse
 
katnel20 profile image
Katie Nelson

How would you handle a cancellation token? I have a need for a timer that should stop firing on cancellation and also the DoWork method should immediately quit.

Collapse
 
solrevdev profile image
John Smith

Hi Katie,

Take a look at this. hitting ctrl-c while its running or in the 5-second DoWork method will cancel gracefully and hopefully do what you want.

I plan to blog about it in more detail later but this should help.

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace tempy
{
public static class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((_, services) => services.AddHostedService<Worker>());
}
}
view raw Program.cs hosted with ❤ by GitHub
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<UserSecretsId>dotnet-tempy-2E552B51-4FEB-4B21-9AFC-4E4C7A69F645</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="3.1.1" />
</ItemGroup>
</Project>
view raw tempy.csproj hosted with ❤ by GitHub
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace tempy
{
/// <summary>
/// Thanks to this post https://stackoverflow.com/a/56666084/2041
/// </summary>
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private Timer _timer;
private Task _executingTask;
private readonly CancellationTokenSource _stoppingCts = new CancellationTokenSource();
public Worker(ILogger<Worker> logger)
{
_logger = logger;
}
public override Task StartAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Worker started at: {time}", DateTimeOffset.Now);
_timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(5));
return Task.CompletedTask;
}
public override async Task StopAsync(CancellationToken stoppingToken)
{
_timer?.Change(Timeout.Infinite, 0);
if (_executingTask == null)
{
await Task.CompletedTask.ConfigureAwait(false);
}
try
{
_logger.LogInformation("Worker stopping at: {time}", DateTimeOffset.Now);
_stoppingCts.Cancel();
}
finally
{
await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, stoppingToken)).ConfigureAwait(false);
}
await Task.CompletedTask.ConfigureAwait(false);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
await Task.CompletedTask.ConfigureAwait(false);
}
public override void Dispose()
{
_timer?.Dispose();
base.Dispose();
}
private void DoWork(object state)
{
GC.KeepAlive(_timer);
_timer?.Change(Timeout.Infinite, 0);
_executingTask = DoWorkAsync(_stoppingCts.Token);
}
private async Task DoWorkAsync(CancellationToken stoppingToken)
{
if (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Worker starting DoWorkAsync at: {time}", DateTimeOffset.Now);
Thread.Sleep(5000);
_logger.LogInformation("Worker completed DoWorkAsync at: {time}", DateTimeOffset.Now);
_timer.Change(TimeSpan.FromSeconds(5), TimeSpan.FromMilliseconds(-1));
await Task.CompletedTask.ConfigureAwait(false);
}
}
}
}
view raw Worker.cs hosted with ❤ by GitHub

Gist here

gist.github.com/solrevdev/f28ab813...

Collapse
 
katnel20 profile image
Katie Nelson

Thanks John. I see now the running timer callback is buried several levels down. Makes things a bit more complex, but I understand what you did.

Thread Thread
 
solrevdev profile image
John Smith

Ahh yes, the timer's TimerCallback method signature needed to be void DoWork(object state) hence needing another method call that uses async Task accepting a CancellationToken.

But looking at it again I think that DoWorkAsync and RunJobAsync could just be the one method.

I'll take another look later and will try and explain more in another blog post.

Thread Thread
 
katnel20 profile image
Katie Nelson

Great! Looking forward to it.

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more