0

I have a UI which spawns off a background worker thread which performs a complex tree of tasks and sub-tasks that takes about a minute to complete.

A requirement is that the background worker task must be capable of being cancelled once it has begun.

At the moment my solution is naive and makes the code a mess. When a cancel button is pressed in the UI, a cancel token is set. The worker thread periodically (between tasks) polls this token and if it is set, it exits:

void ThreadWorkerHandler(CancelToken cancelToken)
{
    DoTask1(cancelToken);
    if (cancelToken.IsSet)
        return;
    DoTask2(cancelToken);
    if (cancelToken.IsSet)
        return;
    DoTask3(cancelToken);
    if (cancelToken.IsSet)
        return;
    DoTask4(cancelToken);
}

void DoTask2(CancelToken cancelToken)
{
    DoSubTask2a();
    if (cancelToken.IsSet)
        return;
    DoSubTask2b();
    if (cancelToken.IsSet)
        return;
    DoSubTask2c();
    if (cancelToken.IsSet)
        return;
}

Is there a better solution? I was toying for something like a SoLongAs statement that would automatically pepper the checks in and automatically and raise an internal exception if the condition was met, which would be internally caught at the end of the loop, eg:

void ThreadWorkerHandler(CancelToken cancelToken)
{
    SoLongAs (canelToken.IsSet == false)
    {
        DoTask1(cancelToken);
        DoTask2(cancelToken);
        DoTask3(cancelToken);
        DoTask4(cancelToken);
    }
}

But I imagine that wouldn't work for some reason, also more importantly I doubt something like this actually exists. If not is there a better way to handle this scenario than I am currently using? Thanks.

Weyland Yutani
  • 4,682
  • 1
  • 22
  • 28

3 Answers3

2

If you have a collection of delegates that represent your work you can get something that looks pretty close to your code snippet. It has a bit more overhead than your intented syntax, but the key point is that it's a constant overhead, rather than a per-line overhead.

List<Action> actions = new List<Action>()
{
    ()=> DoTask1(cancelToken),
    ()=> DoTask2(cancelToken),
    ()=> DoTask3(cancelToken),
    ()=> DoTask4(cancelToken),
};

foreach(var action in actions)
{
    if (!cancelToken.IsSet)
        action();
}
Servy
  • 202,030
  • 26
  • 332
  • 449
  • alternatively `cancelToken.ThrowIfCancellationRequested()`, but that's always seemed a bit..dramatic. – JerKimball Feb 28 '13 at 18:03
  • @JerKimball Yeah, but it doesn't seem all that much better in this specific case – Servy Feb 28 '13 at 18:05
  • Wow! Friends, you are both here. – Ken Kin Feb 28 '13 at 18:07
  • Yeah, one of the intrinsic issues with a cooperative cancellation scheme, I guess. I wonder if you could create a coroutine-like wrapper around the TPL that would bake in the cancellation checks? – JerKimball Feb 28 '13 at 18:08
  • 1
    @JerKimball Well, if you allow the cancellation to be performed at any time then you're essentially back to `Thread.Abort`. The whole idea of cooperative cancellation is that you only allow yourself to be canceled at specific points in which it's okay for you to stop. While you might think that it's okay to just stop any time, odds are you haven't really thought through all of the very nasty things that can happen if you truly allow it. Just knowing *when* it's going to be a problem is very hard. – Servy Feb 28 '13 at 18:10
  • Imagining something slightly different - more like a mini-stack-based-VM, in a way - actions are pushed into a queue, and after each action completes, you auto-check the cancellation token...ok, now I gotta go fire up LINQPad... – JerKimball Feb 28 '13 at 18:14
1

You can use CancellationToken.ThrowIfCancellationRequested(). this will throw exception if token was set.

Also consider using TPL Tasks. All subtasks can be chained one after another with same CancellationToken, this would simplify your code, as TPL framework would take care about checking Token state before invoking continuation.

Your code would looks like this:

Task.Factory.StartNew(DoTask1, cancelationToken)
            .ContinueWith(t => DoTask2(), cancelationToken)
            .ContinueWith(t => DoTask3(), cancelationToken)
            .ContinueWith(t => DoTask4(), cancelationToken)

Note this solution supposing that DoTask<i> will not throw other exceptions except OperationCanceledException.

Note2 you don't have to call ThrowIfCancellationRequested() inside Tasks/subTasks body. TPL will automatically check token state before invoking any continuations. But you can use this method to interrupt execution of task/subtask.

Woodman
  • 1,108
  • 9
  • 11
  • Well, you still end up calling that method all over the place throughout your code if you're doing CPU bound processing, which it seems he is. Shortening it from two lines of code down to one isn't that big of a deal. – Servy Feb 28 '13 at 18:06
  • You may want to add the `OnlyOnCompletion` continuation option, rather than asserting that none of the methods throw an exception. – Servy Feb 28 '13 at 18:12
  • 1
    You don't need to put ThrowIfCancellationRequested inside DoTask. TPL will check token state before invoking continuation – Woodman Feb 28 '13 at 18:13
  • Well, you don't need to add that call unless you *want* it to be able to cancel part way through those operations. If you don't add any checks it will only cancel when each of the tasks is completed (which appears what he's asking in the OP, I'm just clarifying what this code will do). – Servy Feb 28 '13 at 18:18
0

Servy's idea is very good. I'm just stealing it (with all credit to him!) and demonstrating how to use it with an extension method for List<Action>. I'll fully understand anyone that thinks this is "too cute", but I think it has a certain elegance.

Here's an exerpt that show how you can use the extension method. The extension takes a list of Action delegates and runs each one in turn until finished or cancelled, as per Servy's idea.

private static bool test(CancellationToken cancelToken)
{
    return new List<Action> 
    { 
        doTask1,
        doTask2,
        doTask3,
        doTask4,
        () => Console.WriteLine("Press a key to exit.")
    }
    .Run(cancelToken);
}

And here's the entire sample:

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

    namespace ConsoleApplication2
    {
        internal class Program
        {
            private static void Main(string[] args)
            {
                CancellationTokenSource cancelSource = new CancellationTokenSource();

                Console.WriteLine("Press any key to interrupt the work.");
                var work = Task<bool>.Factory.StartNew(() => test(cancelSource.Token));
                Console.ReadKey();
                cancelSource.Cancel();
                Console.WriteLine(work.Result ? "Completed." : "Interrupted.");
            }

            private static bool test(CancellationToken cancelToken)
            {
                return new List<Action> 
                { 
                    doTask1,
                    doTask2,
                    doTask3,
                    doTask4,
                    () => Console.WriteLine("Press a key to exit.")
                }
                .Run(cancelToken);
            }

            private static void doTask1()
            {
                Console.WriteLine("Task 1 Working...");
                Thread.Sleep(1000);
                Console.WriteLine("...did some work.");
            }

            private static void doTask2()
            {
                Console.WriteLine("Task 2 Working...");
                Thread.Sleep(1000);
                Console.WriteLine("...did some work.");
            }

            private static void doTask3()
            {
                Console.WriteLine("Task 3 Working...");
                Thread.Sleep(1000);
                Console.WriteLine("...did some work.");
            }

            private static void doTask4()
            {
                Console.WriteLine("Task 4 Working...");
                Thread.Sleep(1000);
                Console.WriteLine("...did some work.");
            }
        }

        public static class EnumerableActionExt
        {
            public static bool Run(this IEnumerable<Action> actions, CancellationToken cancelToken)
            {
                foreach (var action in actions)
                {
                    if (!cancelToken.IsCancellationRequested)
                    {
                        action();
                    }
                    else
                    {
                        return false;
                    }
                }

                return true;
            }
        }
    }
Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
  • Your extension should probably just accept an `IEnumerable`, not a `List`. There's no need to be so restrictive in a general-purpose method. – Servy Feb 28 '13 at 18:46