1

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()?

  • You are in control of those background threads, and your service has been asked to stop cleanly (StopAsync), it's just that YOU haven't stopped the threads you are controlling. – Neil Feb 14 '22 at 11:19
  • @Neil as you said, the service has been asked to stop cleanly (StopAsync), but why a running background thread stops `StopAsync` being called? –  Feb 14 '22 at 12:07
  • 1
    You have wrapped that "background thread" (aka a thread pool thread running your task in a task object and returned that task back to the framework. Since that task never completes, your program doesn't terminate. Your statements here is similar to saying "I am starting this as a background thread, which I don't care about, and by the way here is a Task object that you can care about wrapping that thread". That is the purpose of the CancellationToken, to make your task care about stopping. Or, you could run your task in a fire and forget manner. – Lasse V. Karlsen Feb 14 '22 at 12:15
  • @LasseV.Karlsen I see what you mean. But somehow my brain is thinking in this way: when Ctrl + C is pressed, all background jobs/threads should stop immediately automatically, which is a OS related thing? –  Feb 14 '22 at 12:34
  • 1
    No, it's not. The hosting framework has specifically captured Ctrl+C in order to do an orderly shutdown of your application. Since your application is refusing to stop, it won't. If that framework hadn't captured Ctrl+C, then yes, your application would've been forcibly terminated. – Lasse V. Karlsen Feb 14 '22 at 14:09
  • @LasseV.Karlsen thank you for you concise answer, you are a legend –  Feb 14 '22 at 22:24
  • You can capture Ctrl+C by hooking into `Console.CancelKeyPress` event, you can find some more information here: https://stackoverflow.com/questions/177856/how-do-i-trap-ctrl-c-sigint-in-a-c-sharp-console-app - if you want to do the same in your own applications. As you can see from the first answer there, this is a CancelEvent, which means the code can set Cancel and thus both reject the cancellation attempt, and also take over shutdown in an orderly fashion. It is this last thing the hosting framework you're using there has done. In short, you need to end your tasks :) – Lasse V. Karlsen Feb 15 '22 at 08:10

2 Answers2

5

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;
    }
}
Mugurel
  • 198
  • 10
  • 1
    yes I can pass the `CancellationToken`to stop the while loop, I understand I can do that, but I don't understand why I have to do that, because the running thread is a background thread, not foreground thread –  Feb 14 '22 at 12:00
  • btw, you code doesn't work, and the logic has flaw(if you run the application and press ctrl + s, it still continue to run in the infinite while loop) shouldn't create a new `CancellationTokenSource`, should use the one from `StartAsync` method. Because there is no connection between ctrl + s and the token you created –  Feb 14 '22 at 12:08
1

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.

Lasse V. Karlsen
  • 380,855
  • 102
  • 628
  • 825
  • V . Karlsen just one more question, could you show me the source code that asp.net core checks any background threadings created in the hosted service, if there is still a thread is running, then it refuse to stop? –  Feb 15 '22 at 11:36
  • 1
    It doesn't check for any running threads, it basically just waits for the task that was returned from StartAsync to complete. I'll see if I can find the code. – Lasse V. Karlsen Feb 15 '22 at 18:49
  • Lasse V. Karlsen thank you. So just to confirm if my understanding is correct: asp.net core framework check status of each Task to see if their status is `IsCompleted`? –  Feb 16 '22 at 00:20
  • 1
    If by "each task" you mean "the tasks returned specifically back to the hosting framework", then yes. If you start tasks but don't return them back, they will not prevent the process from terminating. – Lasse V. Karlsen Feb 16 '22 at 07:41
  • "start tasks but don't return them back", could you give me an example for that, thanks –  Feb 16 '22 at 11:34
  • StartAsync is returning a task, I'm not sure how much clearer I can make it. You should try looking at the code in the github repository. – Lasse V. Karlsen Feb 16 '22 at 11:48
  • I just see what you mean, thank you –  Feb 16 '22 at 12:04