Search code examples
c#servicebackground-servicecancellation-tokencancellationtokensource

Understanding BackgroundService and CancellationToken


I'm quite new to this. There is a lot of information around, and I'm a bit confused. I would like to verify that I understood correctly and appreciate any help. I will number each of my questions in (i) so it would be easier to answer.

I have a BackgroundService with ExecuteAsync(CancellationToken cancellationToken) and StopAsync(CancellationToken cancellationToken).

I assume both methods will get the same token when the service is run. (1) Is this true?

If I understand correctly, I'm supposed to use the cancellationToken in ExecuteAsync to do something as long as cancellationToken is not marked for cancellation. ExecuteAsync has a while loop that runs iterations while cancellationToken is not cancelled. Each iteration awaits something to happen and may launch some async Tasks.

When I launch these tasks, I should give them a CancellationToken from a new CancellationTokenSource that is linked to cancellationToken so that (A) the task knows the stop both when the Service should be cancelled and when the task itself should be cancelled (but, in the latter case, without cancelling the Service itself); and (B) so that cancellationToken exhausted. (2) Are (A) and (B) correct?

Simply calling Cancel() on some CancellationTokenSource does not actually automatically cancels my Tasks or throws an exception. It is up to me to define this logic. (3) Is this true?

Which bring me to another question. Say that I discover insider some task that it was indeed marked for cancellation. What is the correct/expected behavior? Should I clean up the Task's resources and then use ThrowIfCancellationRequested() on the token that I have? (4) Is this true?

Lastly, how should I behave when the service itself is cancelled, i.e., when the main loop of ExecuteAsync discovers that its token is cancelled. (5) Should I immediately ThrowIfCancellationRequested? Should I first clean up resources and only then ThrowIfCancellationRequested? Or should I only call ThrowIfCancellationRequested as the last line of StopAsync?


Solution

  • I have a BackgroundService with ExecuteAsync(CancellationToken cancellationToken) and StopAsync(CancellationToken cancellationToken).

    Don't. You're mixing different levels of abstractions here.

    Either use BackgroundService with ExecuteAsync or use IHostedService with StartAsync and StopAsync.

    You probably just need ExecuteAsync. That's fine for most people.

    I assume both methods will get the same token when the service is run. (1) Is this true?

    No. The cancellation tokens passed to StartAsync, StopAsync and ExecuteAsync are all different.

    If I understand correctly, I'm supposed to use the cancellationToken in ExecuteAsync to do something as long as cancellationToken is not marked for cancellation.

    Yes. The cancellation token passed to ExecuteAsync is cancelled when it's time for the background service to stop.

    When I launch these tasks, I should give them a CancellationToken from a new CancellationTokenSource that is linked to cancellationToken

    Do you ever need to cancel just a single task? If so, then yes. If not, then just pass the cancellationToken on directly.

    Simply calling Cancel() on some CancellationTokenSource does not actually automatically cancels my Tasks or throws an exception. It is up to me to define this logic. (3) Is this true?

    Yes. Cancellation tokens must be observed. This is most commonly done by:

    • Passing them down to any methods that take them. 99% of the time this is all you need to do.
    • (Mainly for CPU-bound tasks) Periodically calling ThrowIfCancellationRequested.
    • (Mainly for I/O-bound tasks) Registering a callback to perform the actual cancellation.

    Which bring me to another question. Say that I discover insider some task that it was indeed marked for cancellation. What is the correct/expected behavior? Should I clean up the Task's resources and then use ThrowIfCancellationRequested() on the token that I have? (4) Is this true?

    Better: always have your resources defined in using statements. Then you can just ThrowIfCancellationRequested if you need to.

    Again, 99% of the time, properly handling cancellation is just a matter of passing the token on down to some other method that already supports cancellation.

    Lastly, how should I behave when the service itself is cancelled, i.e., when the main loop of ExecuteAsync discovers that its token is cancelled. (5) Should I immediately ThrowIfCancellationRequested? Should I first clean up resources and only then ThrowIfCancellationRequested?

    Handle it the same exact way: if your methods are cancellable, just pass it on down. Keep resources in using blocks. If you need to check (most don't), you can periodically call ThrowIfCancellationRequested.

    Specifically:

    Each iteration awaits something to happen and may launch some async Tasks.

    If your "awaits something to happen" is cancellable and your "launch some async Tasks" is cancellable, then just pass the cancellation token to both of these methods. No polling is necessary.

    Or should I only call ThrowIfCancellationRequested as the last line of StopAsync?

    Don't use StopAsync if you're using ExecuteAsync.