How to cancel a task without passing a CancellationTokenSource in .NET?

289 Views Asked by At

I have some business logic that does some long calculations, returns a result and doesn't accept any CancellationTokenSources and, accordingly, doesn't check for it's isCancellationRequested property.

public class Result
{
    
}

public class LongTask
{
    public Result Run()
    {
        Thread.Sleep(10000);
        return new Result();
    }
}

I want my UI (MAUI if it matters) to not only run that business logic and update let's say some Label with recieved result asynchronously, but also have ability to cancel that operation.

So how do I do that? Is that even possible without changing the business logic? Please feel free to ask for details if you need to.

More info: LongTask.Run performs CPU-intensive benchmark. User may enter benchmark parameters that will result in very long waiting so I want user to be able to cancel that operation. I target .NET 7 platform.

3

There are 3 best solutions below

1
Rick Huisman On

Best practice would still be to change the business logic and use the CancellationToken Struct. But what you could do is add a count and when that count is reached then return the Task.

0
JonasH On

TLDR; Yes, it is possible, but not easy.

Cancellation token is a form of cooperative cancellation. Your UI asks the business logic to cancel whatever it is doing, and the business logic cooperates by stopping what it is doing. That is the recommended way to do cancellation. You do not have to use cancellation token specifically, but any replacement would work in more or less the same way, so there is little reason to not just use the standard type.

If you want to abort the work without cooperation you need to put the business logic in a separate process. This is needed because the OS can do cleanup after a process is killed, but it cannot cleanup after a thread. Once upon a time there was a Thread.Abort() method, but that has been removed in any recent .net version, and for good reasons.

The last option is to just let the business logic continue processing, and just throw away the result when it is done. This can let the UI continue to do other things, but you risk a poor user experience if you are running a bunch of processing in the background that no one cares about anymore.

If you have the ability to change the long running task you should just change it to accept a cancellation token. If it is a closed source library, check that you are running the latest version, make a feature request, switch to a better library, or replace it with your own implementation. If there is no way to make it accept a cancellation token you could consider moving it to a separate process or just let it continue in the background.

0
Theodor Zoulias On

The common wisdom for aborting forcefully CPU-bound work in a non-cooperative fashion, is to run the work on a separate OS process and abort the work by killing that process. This is the only way to be absolutely sure that the state of your main process will not be corrupted.

The .NET Framework supports the Thread.Abort method, that can be used for this purpose with considerable danger of corrupting the state of the executable. The .NET Core dropped support for this API. Starting from .NET 7 there is now the option of using the new ControlledExecution.Run API, that performs a controlled Thread.Abort without actually terminating the current thread (the termination is canceled automatically with the Thread.ResetAbort). Using the ControlledExecution.Run method generates a compilation warning:

The ControlledExecution.Run(Action, CancellationToken) method might corrupt the process, and should not be used in production code.

Despite the "controlled" in its name, this API is not less dangerous than the Thread.Abort. Assuming that you understand the risks and you accept them, here is how you could use it:

using CancellationTokenSource cts = new();
cts.CancelAfter(TimeSpan.FromSeconds(10));

Result result;
ControlledExecution.Run(() => { result = LongTask.Run(); }, cts.Token);

To run it asynchronously, just wrap the whole thing in Task.Run.