Should .net core `IHostedService` start a new thread
Asked Answered
M

2

7

.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;
        }
    }
}
Model answered 10/6, 2019 at 12:59 Comment(8)
Hosted services already run in their own threads. There's no point in firing off a new thread.Relative
@ChrisPratt I used to be thinking that hosted services already run in seperated thread. But I found that if the first service I registered blocks the execution, the following services won't be executed...Model
Perhaps during startup, but not during run.Relative
@ChrisPratt From my test, I assume that the hosted services go to a seperated thread when the first await operation is reached. And that is the behaviour of async operations that it may put those async operations to seperated threads, ,but I think it is not guaranteed that await will put the operation to a seperated thread...Model
No, and that's not how async works, anyways. Async is for work that's not thread bound at all. That's why the thread can be released (potentially). When the task completes, the continuation could happen on a new thread or the same thread. The await keyword has nothing to do with anything. It's just syntactic sugar that performs a wait and then unwraps the task.Relative
@ChrisPratt If I understand correctly, all codes after an await is continuation, and they may or may not run in another thread from the origin. From my test code, all host services run in the same thread (because former one will block later ones), and only with a await Task.Delay(xxx) will the following services be running. After the delay times out, the former services could run in parallel with the later ones because former services are continuation and may run in seperated threads. But as you mentioned, it's not guaranteed, so I still need to create thread myself ....Model
Hosted services are independent of each other. The only thing that might hold things up is if you're blocking during service startup, which you shouldn't be doing anyways. Other than that, I have no idea what you're talking about, but you're way off track hereRelative
@ChrisPratt , Hosted services are independent, but they are NOT in seperated threads. You have to create your own thread to make them run in seperated threads.Model
T
12

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.

Tosh answered 10/6, 2019 at 13:20 Comment(8)
Thank you. Do hosted services already run in seperated thread or not? I write my code which registered several services, I found that if my first service block the execution(without a Task.Delay), then following services won't run(there's no printing from the following services).Model
Also could you please explain more about other ways to implement thread function? Do you mean that if I'm writing a thread, I shoulld always change it to a HostedService?Model
@mosakashaka: I don't know if they have a separate thread or not. If you require a separate thread, then use Task.Run instead of Thread.Tosh
@Stephoen I've read some answers about Thread vs Task that says when I need a short-running job, I should use task, while for long-running jobs, I should use thread. So isn't that true?....Also, if I'm having a long-running job which may be blocking, and to run it with a IHostedService, is the correct way to use Task.Run in the StartAsync?Model
No; you should just use Task.Run. I don't know enough about the details of IHostedService to say whether you need Task.Run, but based on your experience it sounds like you do.Tosh
This answer shows a complete misunderstanding of async/await and threads. Just having async and await does not guarantee that your code will run asynchronously nor is Thread obsolete code. It has become more common to use Task.Run where a new Thread was once used but it does not allow all access to all the features of the underlying thread.Warton
@CraigMiller: I love your comment! It's true that async doesn't necessarily mean asynchronous; since the op used await Task.Delay as their example for work, I assumed they had actual asynchronous work to do. If the implementation is synchronous, I still suggest Task.Run. And I do still believe Thread is obsolete for every scenario except COM interop; I do miss thread names when debugging but that's all. There are more modern solutions that are easier to work with than Thread for every scenario except COM interop.Tosh
@CraigMiller I suggest you look up who you are replying to here before saying the person does not understand async/await and threads.Pallas
G
3

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.

Garment answered 8/1, 2021 at 12:40 Comment(2)
StartAsync also does the hostbuilder.Build() command for you. Possibly your log entry is in the wrong position according to this StartAsync? As in, does the main task logging the application has started, or your is that log written within the hosted service? Or even somewhere else? In my case I moved the log about starting the application into the configureservices while creating the hostbuilder (where my logging is configurated, and DI stuff is registering the hosted service). Not a best practice I would say, but hey I'm learning ;)Lum
@Lum the log entry with the Ctrl+C instructions does not come from my code at all. It comes from the .NET Core infrastructure (AFAICT).Garment

© 2022 - 2024 — McMap. All rights reserved.