Home > Mobile >  Why I can stop an IHostedService when therre are background threads running?
Why I can stop an IHostedService when therre are background threads running?

Time:02-16

Below is my simple code that runs an IHostedService

internal class Program {
   public static Task Main(string[] args) {
      var host = new HostBuilder().ConfigureServices((hostcontext, services) => {
         services.AddHostedService<MyService>();
      }).Build();

      host.Run();

      return Task.CompletedTask;
   }
}

public class MyService : IHostedService {

   public Task StartAsync(CancellationToken cancellationToken) {
      return Task.Run(() => {
         while (true) {
            Console.WriteLine("Starting Service");
         }
      });
   }

   public Task StopAsync(CancellationToken cancellationToken) {
      Console.WriteLine("Stopping service");
      return Task.CompletedTask;
   }
}

So when I want to stop the service by pressing ctrl C in the console, I exepct to see the service stops and console prints "Stopping service".

But when I press Ctrl C, the service continues running in the infinite loop, which I don't understand.

I think Task.Run() queues a work item on the thread pool, then a background thread from thread pool to pick up the job, so in my example, it is a worker thread (worker thread's id is 4, main thread's id is 1) that executes the while loop. So when I press ctrl S, the service should stop, then why the running background thread stops the service being stopped, isn't that when a application terminals, all background jobs/ threads terminates too? I mean if Task.Run() runs creates a foreground threads then I can understand, because all foreground threads need to finish before the applciation could be stopped.

P.S:

I can pass the CancellationTokento stop the while loop, I understand I can do that and in the while loop, I check if the token is cancalled etc...

but I don't understand why I have to do that, because the running thread is a background thread, not a foreground thread, so why all background threads need to finish first the the StopAsync() can be invoked? i.e how does a running background thread stops the exection flow reach to StopAsync()?

CodePudding user response:

If you want to stop a Task try using a cancellation token in MyServiceClass

Here is my example:

public class MyService : IHostedService
{
    private CancellationTokenSource _ts;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _ts = new CancellationTokenSource();
        return Task.Factory.StartNew(() =>
        {
            while (true)
            {
                Console.WriteLine("Starting Service");
                Thread.Sleep(500);
        
                if (_ts.Token.IsCancellationRequested)
                {  
                    Console.WriteLine("Stopping service");
                    break;
                }
            }
        }, _ts.Token);
    }


    public Task StopAsync(CancellationToken cancellationToken)
    {
        _ts.Cancel();
        return Task.CompletedTask;
    }
}

CodePudding user response:

The reason why Ctrl C doesn't outright terminate your console application, like you might be used to from "ordinary" console applications, is that there's actually support in .NET to prevent Ctrl C from terminating the application, and for the application to react to Ctrl C and "do stuff".

The hosting framework you're using has used this system, an event, to prevent your application from being forcibly terminated as well as take over shutdown procedure.

The event itself is Console.CancelKeyPress and this is a cancellable event meaning that you can return from the event handler and set a flag on the EventArgs object to signal to the code that invoked your event handler that you want to opt out of default handling of Ctrl C.

The hosting framework has done this.

You can see the exact code here : Microsoft.Extensions.Hosting/Internal/ConsoleLifetime.cs @48-52:

Console.CancelKeyPress  = (sender, e) =>
{
    e.Cancel = true;
    ApplicationLifetime.StopApplication();
};

In addition to cancelling the normal shutdown procedure, the event handler provided by the hosting framework also cancels a CancellationToken that is the token passed to StartAsync. As you say, if you pass this token to your tasks and reacts to it being cancelled, your application shuts down.

The call to ApplicationLifetime.StopApplication eventually ends up in Microsoft.Extensions.Hosting/Internal/ApplicationLifetime.cs @102-112:

private void ExecuteHandlers(CancellationTokenSource cancel)
{
    // Noop if this is already cancelled
    if (cancel.IsCancellationRequested)
    {
        return;
    }

    // Run the cancellation token callbacks
    cancel.Cancel(throwOnFirstException: false);
}

So there you have it. The reason your application keeps spinning in an infinite loop is precisely because the hosting framework prevents ordinary shutdown, and instead give your tasks a chance to complete in an orderly fashion. Since your tasks ignore this request, and just keeps running, your process never terminates when you hit Ctrl C.

  • Related