Making a Non Blocking Synchronous Call Inside a Background Service
Download full source code.
Using asynchronous calls within BackgroundServices
works very well, but occasionally, you might want to use a synchronous call there. However, it will block anything else you have going on, making the background service pointless.
But there is a way to get around this.
The Problem
I have two background services. One has a traditional asynchronous call, and the other has a synchronous call.
Here is the worker with the asynchronous call. It prints the current time, then sleeps for 200 milliseconds.
namespace WorkerWithSyncBackgroundService;
public class WorkerWithAsyncCall : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
Console.WriteLine($"\tWorkerWithAsyncCall: {DateTime.Now.ToLongTimeString()}");
await Task.Delay(200, stoppingToken);
}
}
}
Here is the worker with the synchronous call. It prints the current time, then sleeps for 1000 milliseconds.
namespace WorkerWithSyncBackgroundService;
public class WorkerWithAsyncCall : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
Console.WriteLine($"\tWorkerWithAsyncCall: {DateTime.Now.ToLongTimeString()}");
await Task.Delay(200, stoppingToken);
}
}
}
Add the two workers to the service collection in Program.cs
-
using WorkerWithSyncBackgroundService;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<WorkerWithSyncCall>();
builder.Services.AddHostedService<WorkerWithAsyncCall>();
var host = builder.Build();
host.Run();
When the application runs, the worker with the synchronous call blocks the worker with the asynchronous call. The output will look like this -
WorkerWithSyncCall: 7:55:52 PM. Delaying for 1000ms...
WorkerWithSyncCall: 7:55:53 PM. Delaying for 1000ms...
WorkerWithSyncCall: 7:55:54 PM. Delaying for 1000ms...
WorkerWithSyncCall: 7:55:55 PM. Delaying for 1000ms...
WorkerWithSyncCall: 7:55:56 PM. Delaying for 1000ms...
WorkerWithSyncCall: 7:55:57 PM. Delaying for 1000ms...
WorkerWithSyncCall: 7:55:58 PM. Delaying for 1000ms...
The worker with the asynchronous call cannot run.
The Solutions
I’m going to show a few possible ways to get this working with the synchronous call not blocking the application.
The first is to use a Task.Factory.StartNew
call.
Change the ExecuteAsync
of WorkerWithAsyncCall
to -
1protected override async Task ExecuteAsync(CancellationToken stoppingToken)
2{
3 await Task.Factory.StartNew(() =>
4 {
5 while (!stoppingToken.IsCancellationRequested)
6 {
7 Console.WriteLine($"WorkerWithSyncCall: {DateTime.Now.ToLongTimeString()}. Delaying for 1000ms...");
8 Thread.Sleep(1000);
9 }
10 }, TaskCreationOptions.LongRunning);
11}
Now, when the application start, both workers get a chance to run -
WorkerWithAsyncCall: 8:43:53 PM
WorkerWithSyncCall: 8:43:53 PM. Delaying for 1000ms...
WorkerWithAsyncCall: 8:43:53 PM
WorkerWithAsyncCall: 8:43:54 PM
WorkerWithAsyncCall: 8:43:54 PM
WorkerWithAsyncCall: 8:43:54 PM
WorkerWithSyncCall: 8:43:54 PM. Delaying for 1000ms...
WorkerWithAsyncCall: 8:43:54 PM
WorkerWithAsyncCall: 8:43:55 PM
WorkerWithAsyncCall: 8:43:55 PM
WorkerWithAsyncCall: 8:43:55 PM
WorkerWithAsyncCall: 8:43:55 PM
Another option is to use Task.Run
.
Change the ExecuteAsync
of WorkerWithAsyncCall
to -
1protected override async Task ExecuteAsync(CancellationToken stoppingToken)
2{
3 await Task.Run(() =>
4 {
5 while (!stoppingToken.IsCancellationRequested)
6 {
7 Console.WriteLine($"WorkerWithSyncCall: {DateTime.Now.ToLongTimeString()}. Delaying for 1000ms...");
8 Thread.Sleep(1000);
9 }
10 });
11}
With either of these approaches the synchronous call no longer blocks.
WorkerWithAsyncCall: 8:39:44 PM
WorkerWithAsyncCall: 8:39:44 PM
WorkerWithAsyncCall: 8:39:44 PM
WorkerWithAsyncCall: 8:39:44 PM
WorkerWithAsyncCall: 8:39:44 PM
WorkerWithSyncCall: 8:39:45 PM. Delaying for 1000ms...
WorkerWithAsyncCall: 8:39:45 PM
WorkerWithAsyncCall: 8:39:45 PM
WorkerWithAsyncCall: 8:39:45 PM
WorkerWithAsyncCall: 8:39:45 PM
WorkerWithAsyncCall: 8:39:46 PM
WorkerWithSyncCall: 8:39:46 PM. Delaying for 1000ms..
There is much discussion about which of the two is better, I suggest you review the available documentation.
An alternative
Here is another way, again you should review the C# docs to see if it better for you.
Change the ExecuteAsync
of WorkerWithAsyncCall
to -
1protected override async Task ExecuteAsync(CancellationToken stoppingToken)
2{
3 var thread = new Thread(() =>
4 {
5 while (!stoppingToken.IsCancellationRequested)
6 {
7 Console.WriteLine($"WorkerWithSyncCall: {DateTime.Now.ToLongTimeString()}. Delaying for 1000ms...");
8 Thread.Sleep(1000);
9 }
10 })
11 {
12 IsBackground = true
13 };
14 thread.Start();
15}
Conclusion
You have seen three ways to make synchronous calls in a way that does not block. However, you should review the documentation before choosing which to use.
Download full source code.