MakerOfTheUnicake
MakerOfTheUnicake

Reputation: 141

Feeding a cancellationtoken to a Task does nothing?

I have two examples, straight from microsoft, where these examples seem to have nothing to do with cancellation token, because I can remove the token that is fed to the task, and the result is the same. So my question is: What is the cancellation token for, and why the poor examples? Am I missing something..? :)

using System;
using System.Threading;
using System.Threading.Tasks;
namespace Chapter1.Threads
{
    public class Program
    {
        static void Main()
        {
            CancellationTokenSource cancellationTokenSource =
                new CancellationTokenSource();
            CancellationToken token = cancellationTokenSource.Token;
            Task task = Task.Run(() =>
            {
                while (!token.IsCancellationRequested)
                {
                    Console.Write(“*”);
                    Thread.Sleep(1000);
                }
                token.ThrowIfCancellationRequested();
            }, token);
            try
            {
                Console.WriteLine(“Press enter to stop the task”);
                Console.ReadLine();
                cancellationTokenSource.Cancel();
                task.Wait();
            }  
            catch (AggregateException e)
            {
                Console.WriteLine(e.InnerExceptions[0].Message);
            }
            Console.WriteLine(“Press enter to end the application”);
            Console.ReadLine();
        }
    }
}

Code example2: https://msdn.microsoft.com/en-us/library/system.threading.cancellationtoken(v=vs.110).aspx

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

public class Example
{
   public static void Main()
   {
      // Define the cancellation token.
      CancellationTokenSource source = new CancellationTokenSource();
      CancellationToken token = source.Token;

      Random rnd = new Random();
      Object lockObj = new Object();

      List<Task<int[]>> tasks = new List<Task<int[]>>();
      TaskFactory factory = new TaskFactory(token);
      for (int taskCtr = 0; taskCtr <= 10; taskCtr++) {
         int iteration = taskCtr + 1;
         tasks.Add(factory.StartNew( () => {
                                       int value;
                                       int[] values = new int[10];
                                       for (int ctr = 1; ctr <= 10; ctr++) {
                                          lock (lockObj) {
                                             value = rnd.Next(0,101);
                                          }
                                          if (value == 0) { 
                                             source.Cancel();
                                             Console.WriteLine("Cancelling at task {0}", iteration);
                                             break;
                                          }   
                                          values[ctr-1] = value; 
                                       }
                                       return values;
                                    }, token));   

      }
      try {
         Task<double> fTask = factory.ContinueWhenAll(tasks.ToArray(), 
                                                      (results) => {
                                                         Console.WriteLine("Calculating overall mean...");
                                                         long sum = 0;
                                                         int n = 0; 
                                                         foreach (var t in results) {
                                                            foreach (var r in t.Result) {
                                                                  sum += r;
                                                                  n++;
                                                               }
                                                         }
                                                         return sum/(double) n;
                                                      } , token);
         Console.WriteLine("The mean is {0}.", fTask.Result);
      }   
      catch (AggregateException ae) {
         foreach (Exception e in ae.InnerExceptions) {
            if (e is TaskCanceledException)
               Console.WriteLine("Unable to compute mean: {0}", 
                                 ((TaskCanceledException) e).Message);
            else
               Console.WriteLine("Exception: " + e.GetType().Name);
         }
      }
      finally {
         source.Dispose();
      }
   }
}

Upvotes: 1

Views: 1192

Answers (2)

Brandon
Brandon

Reputation: 4603

I was about to ask a similar question until I found this one. The answer from i3arnon makes sense but I'll add this answer as an addition to hopefully help someone along.

I'll start out by saying (in contrast to the comments on the accepted answer) that the examples from Microsoft on the MSDN are horrible. Unless you already know how Cancellation works, they won't help you much. This MSDN article shows you how to pass a CancellationToken to a Task but if you follow the examples, they never actually show you how to cancel your own currently executing Task. The CancellationToken just vanishes into Microsoft code:

  • await client.GetAsync("http://msdn.microsoft.com/en-us/library/dd470362.aspx", ct);
  • await response.Content.ReadAsByteArrayAsync();

Here are examples of how I use CancellationToken:

When I have a task that needs to continually repeat:

public class Foo
{
    private CancellationTokenSource _cts;

    public Foo()
    {
        this._cts = new CancellationTokenSource();
    }

    public void StartExecution()
    {
        Task.Factory.StartNew(this.OwnCodeCancelableTask, this._cts.Token);
        Task.Factory.StartNew(this.OwnCodeCancelableTask_EveryNSeconds, this._cts.Token);
    }

    public void CancelExecution()
    {
        this._cts.Cancel();
    }

    /// <summary>
    /// "Infinite" loop with no delays. Writing to a database while pulling from a buffer for example.
    /// </summary>
    /// <param name="taskState">The cancellation token from our _cts field, passed in the StartNew call</param>
    private void OwnCodeCancelableTask(object taskState)
    {
        var token = (CancellationToken) taskState;

        while ( !token.IsCancellationRequested )
        {
            Console.WriteLine("Do your task work in this loop");
        }
    }


    /// <summary>
    /// "Infinite" loop that runs every N seconds. Good for checking for a heartbeat or updates.
    /// </summary>
    /// <param name="taskState">The cancellation token from our _cts field, passed in the StartNew call</param>
    private async void OwnCodeCancelableTask_EveryNSeconds(object taskState)
    {
        var token = (CancellationToken)taskState;

        while (!token.IsCancellationRequested)
        {
            Console.WriteLine("Do the work that needs to happen every N seconds in this loop");

            // Passing token here allows the Delay to be cancelled if your task gets cancelled.
            await Task.Delay(1000 /*Or however long you want to wait.*/, token);
        }
    }
}

When I have a task that a user can initiate:

public class Foo
{
    private CancellationTokenSource _cts;
    private Task _taskWeCanCancel;

    public Foo()
    {
        this._cts = new CancellationTokenSource();

        //This is where it's confusing. Passing the token here will only ensure that the task doesn't
        //run if it's canceled BEFORE it starts. This does not cancel the task during the operation of our code.
        this._taskWeCanCancel = new Task(this.FireTheTask, this._cts.Token);
    }
    /// <summary>
    /// I'm not a fan of returning tasks to calling code, so I keep this method void
    /// </summary>
    public void FireTheTask()
    {
        //Check task status here if it's required.
        this._taskWeCanCancel.Start();
    }

    public void CancelTheTask()
    {
        this._cts.Cancel();
    }

    /// <summary>
    /// Go and get something from the web, process a piece of data, execute a lengthy calculation etc...
    /// </summary>
    private async void OurTask()
    {
        Console.WriteLine("Do your work here and check periodically for task cancellation requests...");

        if (this._cts.Token.IsCancellationRequested) return;

        Console.WriteLine("Do another step to your work here then check the token again if necessary...");

        if (this._cts.Token.IsCancellationRequested) return;

        Console.WriteLine("Some work that we need to delegate to another task");

        await Some.Microsoft.Object.DoStuffAsync();
    }

}

Maybe I missed some key feature of Task, but passing the CancellationToken to a Task as anything other than state has never made much sense to me. I have yet to run into a situation where I've passed a CancellationToken to a Task and cancelled the Task before it's run, and even if I did, the first line in every Task I create is always

if (token.IsCancellationRequested) return;

Upvotes: 0

i3arnon
i3arnon

Reputation: 116666

Since cancellation in .Net is cooperative passing a CancellationToken into Task.Run for example is not enough to make sure the task is cancelled.

Passing the token as a parameter only associates the token with the task. It can cancel the task only if it didn't have a chance to start running before the token was cancelled. For example:

var token = new CancellationToken(true); // creates a cancelled token
Task.Run(() => {}, token);

To cancel a task "mid-flight" you need the task itself to observe the token and throw when cancellation is signaled, similar to:

Task.Run(() => 
{
    while (true)
    {
        token.ThrowIfCancellationRequested();
        // do something
    }
}, token);

Moreover, simply throwing an exception from inside the task only marks the task as Faulted. To mark it as Cancelled the TaskCanceledException.CancellationToken needs to match the token passed to Task.Run.

Upvotes: 4

Related Questions