Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using a CancellationToken to cancel a task without explicitly checking within the task?

Background:

I have a web application which kicks off long running (and stateless) tasks:

var task = Task.Run(() => await DoWork(foo))

task.Wait();

Because they are long running, I need to be able to cancel them from a separate web request.

For this, I would like to use a CancellationToken and just throw an exception as soon as the token is canceled. However, from what I've read, Task Cancellation is cooperative, meaning the code the task is running must explicitly check the token to see if a cancellation request has been made (for example CancellationToken.ThrowIfCancellation())

I would like to avoid checking CancellationToken.ThrowIfCancellation() all over the place, since the task is quite long and goes through many functions. I think I can accomplish what I want creating an explicit Thread, but I would really like to avoid manual thread management. That said...

Question: Is it possible to automatically throw an exception in the task when it has been canceled, and if not, are there any good alternatives (patterns, etc.) to reduce polluting the code with CancellationToken.ThrowIfCancellation()?

I'd like to avoid something like this:

async Task<Bar> DoWork(Foo foo)
{
    CancellationToken.ThrowIfCancellation()

    await DoStuff1();

    CancellationToken.ThrowIfCancellation()

    await DoStuff2();

    CancellationToken.ThrowIfCancellation()

    await DoStuff3();
...
}

I feel that this question is sufficiently different from this one because I'm explicitly asking for a way to minimize calls to check the cancellation token, to which the accepted answer responds "Every now and then, inside the functions, call token.ThrowIfCancellationRequested()"

like image 795
schil227 Avatar asked Jul 23 '19 20:07

schil227


3 Answers

Is it possible to automatically throw an exception in the task when it has been canceled, and if not, are there any good alternatives (patterns, etc.) to reduce polluting the code with CancellationToken.ThrowIfCancellation()?

No, and no. All cancellation is cooperative. The best way to cancel code is to have the code respond to a cancellation request. This is the only good pattern.

I think I can accomplish what I want creating an explicit Thread

Not really.

At this point, the question is "how do I cancel uncancelable code?" And the answer to that depends on how stable you want your system to be:

  1. Run the code in a separate Thread and Abort the thread when it is no longer necessary. This is the easiest to implement but the most dangerous in terms of application instability. To put it bluntly, if you ever call Abort anywhere in your app, you should regularly restart that app, in addition to standard practices like heartbeat/smoketest checks.
  2. Run the code in a separate AppDomain and Unload that AppDomain when it is no longer necessary. This is harder to implement (you have to use remoting), and isn't an option in the Core world. And it turns out that AppDomains don't even protect the containing application like they were supposed to, so any apps using this technique also need to be regularly restarted.
  3. Run the code in a separate Process and Kill that process when it is no longer necessary. This is the most complex to implement, since you'll also need to implement some form of inter-process communication. But it is the only reliable solution to cancel uncancelable code.

If you discard the unstable solutions (1) and (2), then the only remaining solution (3) is a ton of work - way, way more than making the code cancelable.

TL;DR: Just use the cancellation APIs the way they were designed to be used. That is the simplest and most effective solution.

like image 166
Stephen Cleary Avatar answered Oct 21 '22 08:10

Stephen Cleary


If you actually just have a bunch of method calls you are calling one after the other, you can implement a method runner that runs them in sequence and checks in between for the cancellation.

Something like this:

public static void WorkUntilFinishedOrCancelled(CancellationToken token, params Action[] work)
{
    foreach (var workItem in work)
    {
        token.ThrowIfCancellationRequested();
        workItem();
    }
}

You could use it like this:

async Task<Bar> DoWork(Foo foo)
{
    WorkUntilFinishedOrCancelled([YourCancellationToken], DoStuff1, DoStuff2, DoStuff3, ...);        
}

This would essentially do what you want.

like image 40
Chillersanim Avatar answered Oct 21 '22 09:10

Chillersanim


If you are OK with the implications of Thread.Abort (disposables not disposed, locks not released, application state corrupted), then here is how you could implement non-cooperative cancellation by aborting the task's dedicated thread.

private static Task<TResult> RunAbortable<TResult>(Func<TResult> function,
    CancellationToken cancellationToken)
{
    var tcs = new TaskCompletionSource<TResult>();
    var thread = new Thread(() =>
    {
        try
        {
            TResult result;
            using (cancellationToken.Register(Thread.CurrentThread.Abort))
            {
                result = function();
            }
            tcs.SetResult(result);
        }
        catch (ThreadAbortException)
        {
            tcs.TrySetCanceled();
        }
        catch (Exception ex)
        {
            tcs.TrySetException(ex);
        }
    });
    thread.IsBackground = true;
    thread.Start();
    return tcs.Task;
}

Usage example:

var cts = new CancellationTokenSource();
var task = RunAbortable(() => DoWork(foo), cts.Token);
task.Wait();
like image 44
Theodor Zoulias Avatar answered Oct 21 '22 08:10

Theodor Zoulias