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

3

7

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()"

Brune answered 23/7, 2019 at 20:48 Comment(5)
"Is it possible to automatically throw an exception in the task when it has been canceled" No. A CancellationToken is for asking "please stop", not for forcefully terminating a task. The task has to "listen" to those requests to stop.Marleen
Thanks @Marleen - so, is there anything I could use (CancellationToken or otherwise) to cancel a task, or am I more or less stuck with this approach?Brune
I'm not sure if there are any alternatives, but I suggest that you follow Microsoft's pattern. It will make integrating with other libraries (like ASP.NET and System.Data.SqlClient) that use Tasks and CancellationTokens much easier.Marleen
1) long-running compute tasks via child threads are generally a bad idea in web apps (the AppPool can be recycled) 2) spinning up a Task only to Wait() kinda defeats the purpose 3) yes you are ”stuck” sadlyAuriscope
@MickyD 1) That's a good point - I'm actually in the process of converting an exe into a web application, so I'll have to analyze that a little more. 2) True, though I just used that as a simple example. 3) ¯\_(ツ)_/¯Brune
S
5

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.

Scenery answered 24/7, 2019 at 1:36 Comment(1)
I was wondering if you would chime in, Mr. Cleary. The points you bring up match what I had been researching, but the insight to the difficulty / instability of doing them is what I needed. I've learned a great deal reading your other posts on here, very grateful for your input.Brune
K
1

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.

Kimbro answered 23/7, 2019 at 22:50 Comment(3)
Yeah, I was kind of thinking in this direction (Decorating the child tasks). It's not as straightforward as my example, but still nice answer.Brune
What happens if one of the methods contains many statements where regular task cancellation check statements are also required?Auriscope
You can use the WorkUntilFinishedOrCancelled within this long running method and split this method into shorter subtasks. The WorkUntilFinishedOrCancelled doesn't handle any thread releated stuff (except for cancellation), so you can call it recursively and repeatedly.Kimbro
S
1

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();
Salpingitis answered 24/7, 2019 at 1:38 Comment(5)
I know you are probably just repeating the OP's code but spinning up a Task only to Wait() it kinda defeats the purpose. Also it might be better to use Task.Run and use the existing thread pool than create an explicit thread via new Thread()Auriscope
@MickyD Aborting thread-pool threads could be even worse, because by the time the notification is received and the thread is aborted, the original work item could have already finished, such that the pool's thread already moved on to another work item.Salpingitis
1) That Stephen Toub quote from .NET Matters was written in March 2006 which predates the .NET 4.0 announcement and 1st .NET 4.0 Public beta in 2008 and 2009 respecively. .NET 4.0 introduced TPL (which introduced async/await and Task). The article refers to the explcit class ThreadPool and ThreadPool.QueueUserWorkItem as per .NET 2 and not "thread pools" as is being described here. Most importantly the article refers to faults that can happen in the ThreadPool if used with QueueUserWorkItem when threads are aborted.Auriscope
2) You calling Thread.Abort() in production code and you're worried ThreadPool.QueueUserWorkItem may start behaving erratically? Doing so will corrupt the corrupt the process in exactly the same way as catching and swallowing unhandled exceptions.Auriscope
@MickyD personally I would use cooperative cancellation, without Thread.Abort. I think that I gave enough hints in my answer that aborting threads is a dangerous practice!Salpingitis

© 2022 - 2024 — McMap. All rights reserved.