DEV Community

Timers in .NET

John Smith on January 27, 2020

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...
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.