DEV Community

John Smith
John Smith

Posted on • Originally published at solrevdev.com on

2 2

Timers in .NET Part 2

I have started to cross-post to the Dev Community website as well as on my solrevdev blog.

A previous post about Timers in .NET received an interesting reply from Katie Nelson who asked about what do do with Cancellation Tokens.

TimerCallBack

The System.Threading.Timer class has been in the original .NET Framework almost from the very beginning and the TimerCallback delegate has a method signature that does not handle CancellationTokens natively.

Trial and error

So, I span up a new dotnet new worker project which has StartAsync and StopAsync methods that take in a CancellationToken in their method signatures and seemed like a good place to start.

After some tinkering with my original class and some research on StackOverflow, I came across this post which I used as the basis as a new improved Timer.

Improvements

Firstly I was able to improve on my original TimerTest class by replacing the field level locking object combined with its’s calls to Monitor.TryEnter(_locker) by using the Timer’s built-in Change method.

Next up I modified the original TimerCallback DoWork method so that it called my new DoWorkAsync(CancellationToken token) method that with a CancellationToken as a parameter does check for IsCancellationRequested before doing my long-running work.

The class is a little more complicated than the original but it does handle ctrlc gracefully.

Source

So, here is the new and improved Timer in a new dotnet core background worker class alongside all of its project files.

{
"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

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 (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more