When should I call CancellationToken.ThrowIfCancellationRequested?
Asked Answered
U

1

7

I developed a C# based Windows Service which runs all of its logic in several different tasks. To allow the service to shutdown gracefully when it is being stopped, I am using a CancellationToken which is passed to any function that accepts one (mostly from 3rd party libraries which I am using) in order to abort processing before completion.

I noticed that none of those functions throw an OperationCanceledException when the cancellation is requested while the function is being called, so my application simply continues executing until I call ThrowIfCancellationRequested() somewhere else later in my code. Am I supposed to manually call ThrowIfCancellationRequested() after calling every single of those functions to make sure that the tasks stop as soon as possible, or when exactly am I supposed to call ThrowIfCancellationRequested() in my own code?

Underhung answered 6/10, 2021 at 14:57 Comment(0)
M
14

Yes, you are supposed to call ThrowIfCancellationRequested() manually, in the appropriate places in your code (where appropriate is determined by you as a programmer).

Consider the following example of a simple job processing function that reads jobs from a queue and does stuff with them. The comments illustrate the sort of thinking the developer might go through when deciding whether to check for cancellation.

Note also that you are right - the standard framework functions that accept a token will not throw a cancellation exception - they will simply return early, so you have to check for cancellation yourself.

public async Task DoWork(CancellationToken token)
{
    while(true)
    {
        // It is safe to check the token here, as we have not started any work
        token.ThrowIfCancellationRequested();

        var nextJob = GetNextJob();

        // We can check the token here, because we have not 
        // made any changes to the system.
        token.ThrowIfCancellationRequested();

        var jobInfo = httpClient.Get($"job/info/{nextJob.Id}", token); 
        // We can check the token here, because we have not 
        // made any changes to the system. 
        // Note that HttpClient won't throw an exception
        // if the token is cancelled - it will just return early, 
        // so we must check for cancellation ourselves.
        token.ThrowIfCancellationRequested();

        // The following code is a critical section - we are going to start
        // modifying various databases and things, so don't check for 
        // cancellation until we have done it all.
        ModifySystem1(nextJob);
        ModifySystem2(nextJob);
        ModifySystem3(nextJob);

        // We *could* check for cancellation here as it is safe, but since
        // we have already done all the required work *and* marking a job 
        // as complete is very fast, there is not a lot of point.
        MarkJobAsCompleted(nextJob);
    }
}

Finally, you might not want to leak cancellation exceptions from your code, because they aren't "real" exceptions - they are expected to occur whenever someone stops your service.

You can catch the exception with an exception filter like so:

public async Task DoWork(CancellationToken token)
{
    try
    {
        while(true)
        { 
            // Do job processing
        }
    }
    catch (OperationCanceledException e) when (e.CancellationToken == token)
    {
        Log.Info("Operation cancelled because service is shutting down.");
    }
    catch (Exception e)
    {
        Log.Error(e, "Ok - this is actually a real exception. Oh dear.");
    }
}
Mu answered 6/10, 2021 at 15:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.