Results of task cancellation in DotNet (C#)

Posted in software by Christopher R. Wirz on Tue Dec 22 2015

The System.Threading.Tasks.Task and System.Threading.Tasks.Task<TResult> classes support cancellation through the use of cancellation tokens. Cancellation tokens support two pattens: [gracefully] return and interrupt[ion].

In many scenarios returning from the task's delegate/method body is perfect. Just keep in mind the task will return as TaskStatus.RanToCompletion and not TaskStatus.Canceled. The other way is to interrupt the cancelled task. You can throw OperationCanceledException or use the CancellationToken API's ThrowIfCancellationRequested method - which is preferred.

Let's try the following code:


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

namespace Practice
{
    class Program
    {
        /// <summary>
        /// A result class
        /// </summary>
        class DemoType{}

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

            _ = Task.Run<DemoType>(() =>
            {
                Thread.Sleep(TimeSpan.FromSeconds(0.5));
                token.ThrowIfCancellationRequested();
                return new DemoType();
            }, token).ContinueWith(t =>
            {
                if (token.IsCancellationRequested)
                {
                    Console.WriteLine("t1 sees CancellationRequested");
                }
                if (t.IsCompleted)
                {
                    Console.WriteLine("t1 is complete");
                }
                if (t.IsCanceled)
                {
                    Console.WriteLine("t1 is canceled");
                }
                if (t.Result != null)
                {
                    Console.WriteLine("t1 result is not null");
                }
                else
                {
                    Console.WriteLine("t1 result IS null");
                }
            });

            _ = Task.Run<DemoType>(() =>
            {
				DemoType returnValue = new DemoType();
				if (token.IsCancellationRequested){return returnValue;}
                Thread.Sleep(TimeSpan.FromSeconds(1));
				if (token.IsCancellationRequested){return returnValue;}
                Thread.Sleep(TimeSpan.FromSeconds(1));
                return returnValue;
            }, token).ContinueWith(t =>
            {
                if (token.IsCancellationRequested)
                {
                    Console.WriteLine("t2 sees CancellationRequested");
                }
                if (t.IsCompleted)
                {
                    Console.WriteLine("t2 is complete");
                }
                if (t.IsCanceled)
                {
                    Console.WriteLine("t2 is canceled");
                }
                if (t.Result != null)
                {
                    Console.WriteLine("t2 result is not null");
                }
                else
                {
                    Console.WriteLine("t2 result IS null");
                }
            });

            _ = Task.Run<DemoType>(() =>
            {
                DemoType returnValue = new DemoType();
				Thread.Sleep(TimeSpan.FromSeconds(1));
                token.ThrowIfCancellationRequested();
                Thread.Sleep(TimeSpan.FromSeconds(1));
                token.ThrowIfCancellationRequested();
                return returnValue;
            }, token).ContinueWith(t =>
            {
                if (token.IsCancellationRequested)
                {
                    Console.WriteLine("t3 sees CancellationRequested");
                }
                if (t.IsCompleted)
                {
                    Console.WriteLine("t3 is complete");
                }
                if (t.IsCanceled)
                {
                    Console.WriteLine("t3 is canceled");
                }
                if (t.Result != null)
                {
                    Console.WriteLine("t3 result is not null");
                }
                else
                {
                    Console.WriteLine("t3 result IS null");
                }
            });

            Console.WriteLine("");
            Console.WriteLine("Press any key to exit...");
            Console.WriteLine("");
            Console.ReadKey();
        }
    }
}

We would expect task 1 (t1) to run to completion. Task 2 and 3 won't run to completion, but task 2 (t2) ends gracefully while task 3 (t3) will be interrupted.

The results at the console are:

Task 1 ran to completion. Task 2 returned a result when canceled, reporting as complete. Task 3 did not return a result, but reported as Canceled.

As it turns out, you can't even get the result of a task that has been interrupted (task 3). Maybe the source code of Result will provide some answers...


[DebuggerBrowsable(DebuggerBrowsableState.Never)]
public TResult Result
{
	get
	{
		if (!base.IsWaitNotificationEnabledOrNotRanToCompletion)
		{
			return m_result;
		}
		// return GetResultCore(waitCompletionNotification: true);
		if (!base.IsCompleted)
		{
			InternalWait(-1, default(CancellationToken));
		}
		if (waitCompletionNotification)
		{
			NotifyDebuggerOfWaitCompletionIfNecessary();
		}
		if (!base.IsCompletedSuccessfully)
		{
			// ThrowIfExceptional(includeTaskCanceledExceptions: true);
			// Exception exceptions = GetExceptions(includeTaskCanceledExceptions);
			Exception exceptions = null;
			if (includeTaskCanceledExceptions && IsCanceled)
			{
				exceptions = new TaskCanceledException(this);
			}
			if (ExceptionRecorded)
			{
				return m_contingentProperties.m_exceptionsHolder.CreateExceptionObject(calledFromFinalizer: false, exceptions);
			}
			if (exceptions != null)
			{
				return new AggregateException(exceptions);
			}
			if (exceptions != null)
			{
				UpdateExceptionObservedStatus();
				throw exceptions;
			}
		}
		return m_result;
	}
}

So what does this tell us? Cancel a task gracefully if you can and avoid chaining and nesting Tasks. Of course, there is nothing wrong with doing that, but as you can see it just makes more work later. You can also checkout other studies that discuss the 15x overhead associated with the interrupt pattern.