Proper task cancellation in DotNet (C#)

Posted in software by Christopher R. Wirz on Sun Dec 20 2015

The System.Threading.Tasks.Task and System.Threading.Tasks.Task<TResult> classes support cancellation through the use of cancellation tokens. But what is the right way of doing this? The Task.Run API takes a cancellation token, but is this enough? If we want to actually end a task when we attempt a cancellation, how do we do it?

Let's compare.... We'll try one task passing a token into just Task.Run. We'll try another task that also handles the token in the delegate. THen we'll try a task that only handles the token in the delegate.

Let's try the following code:


using System;
using System.Threading;
using System.Threading.Tasks;

namespace Practice
{
    class Program
    {
        /// <summary>
        /// The main method
        /// </summary>
        /// <param name="args">Arguments not used for example</param>
        static void Main(string[] args)
        {
            var cts = new CancellationTokenSource();
            cts.CancelAfter(TimeSpan.FromSeconds(2));
            var token = cts.Token;

			// Try a task just passing the token
            _ = Task.Run(() =>
            {
                for (int i = 1; i <= 5; i++)
                {
                    Thread.Sleep(TimeSpan.FromSeconds(1));
                    Console.WriteLine(i + " seconds passed in thread 1");
                }
            }, token);

			// Try a task in which the delegate handles the token, and the token is passed
            _ = Task.Run(() =>
            {
                for (int i = 1; i <= 5; i++)
                {
                    Thread.Sleep(TimeSpan.FromSeconds(1));
                    Console.WriteLine(i + " seconds passed in thread 2");
                    if (token.IsCancellationRequested) { return; }
                }
            }, token);

			// Try a task in which only the delegate handles the token
            _ = Task.Run(() =>
            {
                for (int i = 1; i <= 5; i++)
                {
                    Thread.Sleep(TimeSpan.FromSeconds(1));
                    Console.WriteLine(i + " seconds passed in thread 3");
                    if (token.IsCancellationRequested) { return; }
                }
            });
			
            Console.WriteLine("");
            Console.WriteLine("Press any key to exit...");
            Console.WriteLine("");
            Console.ReadKey();
        }
    }
}

We would expect Task 1 to run to completion. Task 2 will end when canceled. What about Task 3?

The results at the console are:

Task 1 ran to completion. Task 2 ended when canceled. Task 3 ended when canceled.

So what does this tell us? A token must be used in the delegate of a task body in order to be effective. A token passed into Task.Run only will prevent the task from running if it has not run yet and is still scheduled to run.