Should .net core `IHostedService` start a new thread

Viewed 6638

.net core BackgroundService or IHostedService's start method is async:

//IHostedService
Task StartAsync(CancellationToken cancellationToken);
//BackgroundService
Task ExecuteAsync(CancellationToken stoppingToken);

So should I write all the logic in the ExecuteAsync/StartAsync method, or should I just start a new thread and return right away?

For example, which of the following two is the correct implementation?

1.

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    new Thread(async () => await DoWork(stoppingToken)).Start();

    await Task.CompletedTask;
}

private async Task DoWork(CancellationToken stoppingToken) 
{
    while (!stoppingToken.IsCancellationRequested)
        //actual works
}

2.

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    while (!stoppingToken.IsCancellationRequested)
    {
        //actual works
        await Task.Delay(1000);//e.g
    }
}

Semantically I think the second one seems to be right, but if there're several IHostedServices, can they run in parallel with the second form?

Edit 1

I also write a sample program that illustrate the hosted services aren't themselves run as seperated threads.

The message "Waiting for signal.." won't be written console until I type a q:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace BackgroundTaskTest
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var host = new HostBuilder()
                .ConfigureServices((hostContext, services) =>
                {
                    IConfiguration config = hostContext.Configuration;

                    //register tasks
                    services.AddHostedService<ReadService>();
                    services.AddHostedService<BlockService>();
                })
                .UseConsoleLifetime()
                .Build();

            await host.RunAsync();
        }
    }

    public static class WaitClass
    {
        public static AutoResetEvent Event = new AutoResetEvent(false);
    }

    public class ReadService : BackgroundService
    {
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                var c = Console.ReadKey();
                if (c.KeyChar == 'q')
                {
                    Console.WriteLine("\nTrigger event");
                    WaitClass.Event.Set();
                }
                await Task.Delay(1);
            }
        }
    }

    public class BlockService : BackgroundService
    {
        protected override Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                Console.WriteLine("Waiting for signal..");
                WaitClass.Event.WaitOne();
                Console.WriteLine("Signal waited");
            }
            return Task.CompletedTask;
        }
    }
}
2 Answers

Just implement it using async/await, as your second example shows. There is no need for an extra Thread.

Side note: Thread should only be used for COM interop; there are far better solutions these days for every other former use case of Thread. As soon as you type new Thread, you already have legacy code.

For StartAsync(), at least, you should start a separate thread. If your job is going to take a significant length of time to complete. Otherwise -- at least in my experience, with .NET Core 2.1 on Windows 10 -- the IHostedService won't register as "started" until all the work is done.

I have a test console app that HTTP-GETs the same URL 360 times, waiting 10 seconds between each, and counting how many times it succeeds and how many times it fails. In this app, I have configured ILogger logging with NLog, going both to a text file and to the console. Highest level of logging verbosity for each.

Once the hosted application finishes starting, I see a message logged to the console (and to the file), telling me that the hosted application has started, and that I can press Ctrl+C to quit.

If in StartAsync(), I simply await the test method that does all the work, then I don't see that message logged until all the work is done, over an hour later. If instead, I kick off a new thread as in your first example, then I see the Ctrl+C instructions almost immediately, as I should.

So empirically, your first example appears to be correct.

Related