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.

comments powered by Disqus

Related