Understanding CancellationTokenSource with Tasks: Part 2

After some more testing and digging, I have discovered a way to keep Task cancellation behavior more consistent. In my previous post, I was allowing the Action within each Task to exit gracefully when it evaluated the IsCancellationRequested property of the CancellationToken. It turns out that this is why the Task wrapping that Action did not report a “true” result for IsCanceled once the Action exited. In order for the Task to know that its Action was canceled (at least once that Action is running as we established before), the Action has to throw the TaskCanceledException. It does this by calling the ThrowIfCancellationRequested method on the CancellationToken object that it was given. See this unit test for an example:

[TestMethod]
public void RunningTaskDoesRespondToThrowInsideAction()
{
	CancellationTokenSource tokenSource = new CancellationTokenSource();
	var token = tokenSource.Token;
	Task taskWithToken = new Task(
		() =>
		{
			while (true)
			{
				token.ThrowIfCancellationRequested();
			}
		}, token
	);
	taskWithToken.Start();
	while (taskWithToken.Status != TaskStatus.Running)
	{
		//Wait until task is running
	}
	if (taskWithToken.Status != TaskStatus.Faulted)
	{
		tokenSource.Cancel();
		bool taskThrewExceptionUponWait = false;
		try
		{
			taskWithToken.Wait();
		}
		catch (Exception ex)
		{
			Assert.IsInstanceOfType(ex, typeof(AggregateException));
			var inner = ((AggregateException)ex).InnerExceptions[0];
			Assert.IsInstanceOfType(inner, typeof(TaskCanceledException));
			taskThrewExceptionUponWait = true;
		}
		Assert.IsTrue(taskWithToken.Status == TaskStatus.Canceled);
		Assert.IsTrue(taskThrewExceptionUponWait);
	}
	else
	{
		Assert.Inconclusive("Task faulted");
		try
		{
			tokenSource.Cancel(); // Clean up
		}
		catch { }
	}
}

You might think to yourself now, as I did, “well, if throwing a specific exception type is what notifies the Task of cancellation, does the Task really need a reference to the token in this case?”. It turns out that yes, it does. Without the reference to the Token in the Task constructor, the Action’s “throw” results in the Task being faulted instead of canceled. See this example:

[TestMethod]
public void TaskWithoutTokenReportsNotCanceled_EvenWhenActionThrows()
{
	CancellationTokenSource tokenSource = new CancellationTokenSource();
	var token = tokenSource.Token;
	Task taskWithToken = new Task(
		() =>
		{
			while (true)
			{
				token.ThrowIfCancellationRequested();
			}
		} //No token
	);
	taskWithToken.Start();
	while (taskWithToken.Status != TaskStatus.Running)
	{
		//Wait until task is running
	}
	if (taskWithToken.Status != TaskStatus.Faulted)
	{
		tokenSource.Cancel();
		bool taskThrewExceptionUponWait = false;
		try
		{
			taskWithToken.Wait();
		}
		catch (Exception ex)
		{
			Assert.IsInstanceOfType(ex, typeof(AggregateException));
			var inner = ((AggregateException)ex).InnerExceptions[0];
			Assert.IsInstanceOfType(inner, typeof(OperationCanceledException));
			taskThrewExceptionUponWait = true;
		}
		Assert.IsFalse(taskWithToken.Status == TaskStatus.Canceled);
		Assert.IsTrue(taskWithToken.Status == TaskStatus.Faulted);
		Assert.IsTrue(taskThrewExceptionUponWait);
	}
	else
	{
		Assert.Inconclusive("Task faulted");
		try
		{
			tokenSource.Cancel(); // Clean up
		}
		catch { }
	}
}

So, Aha! We figured out some more about this. I will follow up with another post detailing results from Parallel.For, Parallel.ForEach and PLINQ with CancellationTokens as well.

Advertisements

About John Adams

Microsoft Azure specialist and trainer, software developer, ASP.NET MVC enthusiast, father, husband, Christian. Overall, a very fortunate man.
This entry was posted in Asynchrony, Unit Tests and tagged , . Bookmark the permalink.

5 Responses to Understanding CancellationTokenSource with Tasks: Part 2

  1. Pingback: Short trip through Task, CancellationTokenSource and Parallel « a developer’s breadcrumb

  2. E says:

    I see…faulted vs cancelled. Thx.

  3. E says:

    Only helpful way that I can see to use TaskContinuationOptions.OnlyOnCanceled is like this:
    try
    {
    _task.ContinueWith(t => Console.WriteLine(“######## Task cancelled ########”), TaskContinuationOptions.OnlyOnCanceled);
    }
    catch (TaskCanceledException _ex)
    {
    // handle it or something…
    }

    • E says:

      Sry i don’t know if that was quite right. But my point is, it seems that task cancellation is not supposed to happen (since it insists on involving an exception), and that we should allow the task to finish or timeout, I guess.

  4. niranjankala says:

    Reblogged this on .Net Development Stack and commented:
    Part-2

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s