Cancelling a Task is throwing an exception

C#MultithreadingC# 4.0

C# Problem Overview


From what I've read about Tasks, the following code should cancel the currently executing task without throwing an exception. I was under the impression that the whole point of task cancellation was to politely "ask" the task to stop without aborting threads.

The output from the following program is:

> Dumping exception > > [OperationCanceledException] > > Cancelling and returning last calculated prime.

I am trying to avoid any exceptions when cancelling. How can I accomplish this?

void Main()
{
    var cancellationToken = new CancellationTokenSource();
	
	var task = new Task<int>(() => {
		return CalculatePrime(cancellationToken.Token, 10000);
	}, cancellationToken.Token);
	
	try
    {
	    task.Start();
        Thread.Sleep(100);
        cancellationToken.Cancel();
        task.Wait(cancellationToken.Token);    		
	}
    catch (Exception e)
    {
        Console.WriteLine("Dumping exception");
        e.Dump();
	}
}

int CalculatePrime(CancellationToken cancelToken, object digits)
{  
	int factor; 
	int lastPrime = 0;

 	int c = (int)digits;
	
	for (int num = 2; num < c; num++)
    { 
        bool isprime = true;
        factor = 0; 
	
        if (cancelToken.IsCancellationRequested)
        {
            Console.WriteLine ("Cancelling and returning last calculated prime.");
            //cancelToken.ThrowIfCancellationRequested();
            return lastPrime;
        }
		
        // see if num is evenly divisible 
        for (int i = 2; i <= num/2; i++)
        { 
            if ((num % i) == 0)
            {             
                // num is evenly divisible -- not prime 
                isprime = false; 
                factor = i; 
            }
        } 
 	
        if (isprime)
        {
            lastPrime = num;
        }
    }
	
    return lastPrime;
}

C# Solutions


Solution 1 - C#

> I am trying to avoid any exceptions when cancelling.

You shouldn't do that.

Throwing OperationCanceledException is the idiomatic way that "the method you called was cancelled" is expressed in TPL. Don't fight against that - just expect it.

It's a good thing, because it means that when you've got multiple operations using the same cancellation token, you don't need to pepper your code at every level with checks to see whether or not the method you've just called has actually completed normally or whether it's returned due to cancellation. You could use CancellationToken.IsCancellationRequested everywhere, but it'll make your code a lot less elegant in the long run.

Note that there are two pieces of code in your example which are throwing an exception - one within the task itself:

cancelToken.ThrowIfCancellationRequested()

and one where you wait for the task to complete:

task.Wait(cancellationToken.Token);

I don't think you really want to be passing the cancellation token into the task.Wait call, to be honest... that allows other code to cancel your waiting. Given that you know you've just cancelled that token, it's pointless - it's bound to throw an exception, whether the task has actually noticed the cancellation yet or not. Options:

  • Use a different cancellation token (so that other code can cancel your wait independently)

  • Use a time-out

  • Just wait for as long as it takes

Solution 2 - C#

You are explicitly throwing an Exception on this line:

cancelToken.ThrowIfCancellationRequested();

If you want to gracefully exit the task, then you simply need to get rid of that line.

Typically people use this as a control mechanism to ensure the current processing gets aborted without potentially running any extra code. Also, there is no need to check for cancellation when calling ThrowIfCancellationRequested() since it is functionally equivalent to:

if (token.IsCancellationRequested) 
    throw new OperationCanceledException(token);

When using ThrowIfCancellationRequested() your Task might look more like this:

int CalculatePrime(CancellationToken cancelToken, object digits) {
    try{
        while(true){
            cancelToken.ThrowIfCancellationRequested();
            
            //Long operation here...
        }
    }
    finally{
        //Do some cleanup
    }
}

Also, Task.Wait(CancellationToken) will throw an exception if the token was cancelled. To use this method, you will need to wrap your Wait call in a Try...Catch block.

MSDN: How to Cancel a Task

Solution 3 - C#

Some of the above answers read as if ThrowIfCancellationRequested() would be an option. It is not in this case, because you won't get your resulting last prime. The idiomatic way that "the method you called was cancelled" is defined for cases when canceling means throwing away any (intermediate) results. If your definition of cancelling is "stop computation and return the last intermediate result" you already left that way.

Discussing the benefits especially in terms of runtime is also quite misleading: The implemented algorithm sucks at runtime. Even a highly optimized cancellation will not do any good.

The easiest optimization would be to unroll this loop and skip some unneccessary cycles:

for(i=2; i <= num/2; i++) { 
  if((num % i) == 0) { 
    // num is evenly divisible -- not prime 
    isprime = false; 
    factor = i; 
  }
} 

You can

  • save (num/2)-1 cycles for every even number, which is slightly less than 50% overall (unrolling),
  • save (num/2)-square_root_of(num) cycles for every prime (choose bound according to math of smallest prime factor),
  • save at least that much for every non-prime, expect much more savings, e.g. num = 999 finishes with 1 cycle instead of 499 (break, if answer is found) and
  • save another 50% of cycles, which is of course 25% overall (choose step according to math of primes, unrolling handles the special case 2).

That accounts to saving a guaranteed minimum of 75% (rough estimation: 90%) of cycles in the inner loop, just by replacing it with:

if ((num % 2) == 0) {
  isprime = false; 
  factor = 2;
} else {
  for(i=3; i <= (int)Math.sqrt(num); i+=2) { 
    if((num % i) == 0) { 
      // num is evenly divisible -- not prime 
      isprime = false; 
      factor = i;
      break;
    }
  }
} 

There are much faster algorithms (which I won't discuss because I'm far enough off-topic) but this optimization is quite easy and still proves my point: Don't worry about micro-optimizing runtime when your algorithm is this far from optimal.

Solution 4 - C#

Another note about the benefit of using ThrowIfCancellationRequested rather than IsCancellationRequested: I've found that when needing to use ContinueWith with a continuation option of TaskContinuationOptions.OnlyOnCanceled, IsCancellationRequested will not cause the conditioned ContinueWith to fire. ThrowIfCancellationRequested, however, will set the Canceled condition of the task, causing the ContinueWith to fire.

Note: This is only true when the task is already running and not when the task is starting. This is why I added a Thread.Sleep() between the start and cancellation.

CancellationTokenSource cts = new CancellationTokenSource();

Task task1 = new Task(() => {
    while(true){
        if(cts.Token.IsCancellationRequested)
            break;
    }
}, cts.Token);
task1.ContinueWith((ant) => {
    // Perform task1 post-cancellation logic.
    // This will NOT fire when calling cst.Cancel().
}

Task task2 = new Task(() => {
    while(true){
        cts.Token.ThrowIfCancellationRequested();
    }
}, cts.Token);
task2.ContinueWith((ant) => {
    // Perform task2 post-cancellation logic.
    // This will fire when calling cst.Cancel().
}

task1.Start();
task2.Start();
Thread.Sleep(3000);
cts.Cancel();

Solution 5 - C#

You have two things listening to the token, the calculate prime method and also the Task instance named task. The calculate prime method should return gracefully, but task gets cancelled while it is still running so it throws. When you construct task don't bother giving it the token.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionRazorView Question on Stackoverflow
Solution 1 - C#Jon SkeetView Answer on Stackoverflow
Solution 2 - C#JoshView Answer on Stackoverflow
Solution 3 - C#No answerView Answer on Stackoverflow
Solution 4 - C#Gerard TorresView Answer on Stackoverflow
Solution 5 - C#andrew pateView Answer on Stackoverflow