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>()); | |
} | |
} |
<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> |
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); | |
} | |
} | |
} | |
} |
Success 🎉
Top comments (0)