Domanda

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.

È stato utile?

Soluzione

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();
}

Altri suggerimenti

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.

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;
            }
        }
    }
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top