Devesh
Devesh

Reputation: 394

Cancellation Token source and nested tasks

I have a doubt with cancellation token source which I am using as shown in the below code:

    void Process()
    {
        //for the sake of simplicity I am taking 1, in original implementation it is more than 1
        var cancellationToken = _cancellationTokenSource.Token;
        Task[] tArray = new Task[1];
        tArray[0] = Task.Factory.StartNew(() =>
        {
            cancellationToken.ThrowIfCancellationRequested();
            //do some work here
            MainTaskRoutine();
        }, cancellationToken);

        try
        {
            Task.WaitAll(tArray);
        }
        catch (Exception ex)
        {
            //do error handling here
        }
    }

    void MainTaskRoutine()
    {
        //for the sake of simplicity I am taking 1, in original implementation it is more than 1
        //this method shows that a nested task is created 
        var cancellationToken = _cancellationTokenSource.Token;
        Task[] tArray = new Task[1];
        tArray[0] = Task.Factory.StartNew(() =>
        {
            cancellationToken.ThrowIfCancellationRequested();
            //do some work here

        }, cancellationToken);

        try
        {
            Task.WaitAll(tArray);
        }
        catch (Exception ex)
        {
         //do error handling here
        }
    }


Edit: further elaboration

End objective is: when a user cancels the operation, all the immediate pending tasks(either children or grand children) should cancel.

Scenario: As per above code: 1. I first check whether user has asked for cancellation 2. If user has not asked for cancellation then only continue with the task (Please see Process method). sample code shows only one task here but actually there can be three or more

Lets say that CPU started processing Task1 while other tasks are still in the Task queue waiting for some CPU to come and execute them. User requests cancellation: Task 2,3 in Process method are immediately cancelled, but Task 1 will continue to work since it is already undergoing processing.

In Task 1 it calls method MainTaskRoutine, which in turn creates more tasks.

In the function of MainTaskRoutine I have written: cancellationToken.ThrowIfCancellationRequested();

So the question is: is it correct way of using CancellationTokenSource as it is dependent on Task.WaitAll()?

Upvotes: 3

Views: 5233

Answers (2)

Devesh
Devesh

Reputation: 394

After doing some research I found this link.

The code now looks like this: see the usage of CancellationTokenSource.CreateLinkedTokenSource in below code

    void Process()
    {
        //for the sake of simplicity I am taking 1, in original implementation it is more than 1
        var cancellationToken = _cancellationTokenSource.Token;
        Task[] tArray = new Task[1];
        tArray[0] = Task.Factory.StartNew(() =>
        {
            cancellationToken.ThrowIfCancellationRequested();
            //do some work here
            MainTaskRoutine(cancellationToken);
        }, cancellationToken);

        try
        {
            Task.WaitAll(tArray);
        }
        catch (Exception ex)
        {
            //do error handling here
        }
    }

    void MainTaskRoutine(CancellationToken cancellationToken)
    {
        //for the sake of simplicity I am taking 1, in original implementation it is more than 1
        //this method shows that a nested task is created 

        using (var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
        {
            var cancelToken = cancellationTokenSource.Token;
            Task[] tArray = new Task[1];
            tArray[0] = Task.Factory.StartNew(() =>
            {
                cancelToken.ThrowIfCancellationRequested();
                //do some work here

            }, cancelToken);

            try
            {
                Task.WaitAll(tArray);
            }
            catch (Exception ex)
            {
                //do error handling here
            } 
        }
    }

Note: I haven't used it, but I will let You know once it is done :)

Upvotes: 1

noseratio
noseratio

Reputation: 61686

[EDITED] As you use an array in your code, I assume there could be multiple tasks, not just one. I also assume that within each task that you're starting from Process you want to do some CPU-bound work first (//do some work here), and then run MainTaskRoutine.

How you handle task cancellation exceptions is determined by your project design workflow. E.g., you could do it inside Process method, or from where you call Process. If your only concern is to remove Task objects from the array where you keep track of the pending tasks, this can be done using Task.ContinueWith. The continuation will be executed regardless of the task's completion status (Cancelled, Faulted or RanToCompletion):

Task Process(CancellationToken cancellationToken)
{
    var tArray = new List<Task>();
    var tArrayLock = new Object();

    var task = Task.Run(() =>
    {
        cancellationToken.ThrowIfCancellationRequested();
        //do some work here

        return MainTaskRoutine(cancellationToken);
    }, cancellationToken);

    // add the task to the array,
    // use lock as we may remove tasks from this array on a different thread
    lock (tArrayLock)
        tArray.Add(task);
    task.ContinueWith((antecedentTask) =>
    {
        if (antecedentTask.IsCanceled || antecedentTask.IsFaulted)
        {
            // handle cancellation or exception inside the task
            // ...
        }
        // remove task from the array,
        // could be on a different thread from the Process's thread, use lock
        lock (tArrayLock)
            tArray.Remove(antecedentTask);
    }, TaskContinuationOptions.ExecuteSynchronously);

    // add more tasks like the above
    // ...

    // Return aggregated task
    Task[] allTasks = null;
    lock (tArrayLock)
        allTasks = tArray.ToArray();
    return Task.WhenAll(allTasks);
}

Your MainTaskRoutine can be structured in exactly the same way as Process, and have the same method signature (return a Task).

Then you may want to perform a blocking wait on the aggregated task returned by Process, or handle its completion asynchronously, e.g:

// handle the completion asynchronously with a blocking wait
void RunProcessSync()
{
    try
    {
        Process(_cancellationTokenSource.Token).Wait();
        MessageBox.Show("Process complete");
    }
    catch (Exception e)
    {
        MessageBox.Show("Process cancelled (or faulted): " + e.Message);
    }
}

// handle the completion asynchronously using ContinueWith
Task RunProcessAync()
{
    return Process(_cancellationTokenSource.Token).ContinueWith((task) =>
    {
        // check task.Status here
        MessageBox.Show("Process complete (or cancelled, or faulted)");
    }, TaskScheduler.FromCurrentSynchronizationContext());
}

// handle the completion asynchronously with async/await
async Task RunProcessAync()
{
    try
    {
        await Process(_cancellationTokenSource.Token);
        MessageBox.Show("Process complete");
    }
    catch (Exception e)
    {
        MessageBox.Show("Process cancelled (or faulted): " + e.Message);
    }
}

Upvotes: 4

Related Questions