Search code examples
c#asp.netasynchronousasync-awaitcancellation-token

Cancel method which doesn't accept cancellation token


I need to cancel an async method while is in the middle of the process.

For what I've seen the recommendation is the CancelationToken, which can be used in different ways:

  1. Cancel the Task before it starts.
  2. Register a callback method which should handle the cancelation of method I intend to cancel (Register callbacks for cancellation requests).
  3. Passing the cancellation token to the method, which should then check if the token has been cancelled or not.

The problem is I cannot use any of these solutions. I saw the option of Thread.Abort, but it seems now deprecated; SYSLIB0006: Thread.Abort is not supported. And the Task doesn't provide any cancellation method itself.

But my ideal solution would be something similar to Abort. Or like when using PowerShell we press Ctrl+C.

My app runs in a similar way as a PowerShell script. I have a form where I input some parameters and then run an async method based on them. But I might need to cancel this method in the middle for whatever reason (taking too long, need to change parameters). So, the method will always start, and either registering a callback or passing the CancelationToken would bring similar output. I would have to fill the method with checks on the cancelation token status in every corner, which seems not the most efficient way as the method is not a simple loop.

Is there any possibility to simply cut/abort/cancel a method/Task?

I've tried something like below sample. Although I get to return before the method completes, this is still running in the background, which I cannot allow.

private static async Task<decimal> LongRunningOperationWithCancellationTokenAsync(
    int loop, CancellationToken cancellationToken)
{
    // We create a TaskCompletionSource of decimal
    var taskCompletionSource = new TaskCompletionSource<decimal>();

    // Registering a lambda into the cancellationToken
    cancellationToken.Register(() =>
    {
        // We received a cancellation message, cancel the TaskCompletionSource.Task
        taskCompletionSource.TrySetCanceled();
    });

    var task = LongRunningOperation(loop);

    // Wait for the first task to finish among the two
    var completedTask = await Task.WhenAny(task, taskCompletionSource.Task);

    // If the completed task is our long running operation we set its result.
    if (completedTask == task)
    {
        // Extract the result
        // The task is finished and the await will return immediately
        var result = await task;

        // Set the taskCompletionSource result
        taskCompletionSource.TrySetResult(result);
    }

    // Return the result of the TaskCompletionSource.Task
    return await taskCompletionSource.Task;
}

Solution

  • Cancellation in .NET (and most modern languages) is cooperative. This means that the application code can request some operation to cancel, and the operation must respond to that cancellation request in order for the cancellation to take effect.

    For what I've seens the recommendation is the Cancelation Token, which can be used in differen ways:

    There are two general ways of responding to cancellation. One is using Register to register a callback that is invoked when cancellation is requested. Another is periodically polling the cancellation token to see if it has been canceled (usually best done via ThrowIfCancellationRequested, not IsCancellationRequested).

    But my ideal solution would be something similar to Abort. Or like when using PowerShell we press Ctrl+C.

    The reason that Thread.Abort was discontinued and that the replacement ControlledExecution.Run has loud warnings about it is the same reason that most modern languages only have cooperative cancellation: rudely aborting code is not good for the hosting process. Resources get left open, locks remain held, unmanaged code has its own issues - just in general, the state of the entire application/process can become corrupted in difficult-to-predict ways.

    In the .NET Framework days, they tried really hard to work around these: giving weird behavior to catch blocks handling ThreadAbortException, constrained execution regions, attempting to sandbox code into AppDomains, etc. But at the end of it all, the rude-abort approach is just too unpredictable.

    I would have to fill the method with checks on the cancelation token status in every corner, which seems not the most efficient way as the method is not a simple loop.

    This sounds like it might be the best solution. You don't really need them everywhere; usually a few strategically-placed checks are sufficient to respond to cancellation in a timely-enough manner. Since your app sounds like a pipeline, consider checking whenever a step/transformer pulls an item from its channel/input. Any items already being processed are completed; it just won't pull the next one.

    Is there any possibility to simply cut/abort/cancel a method/Task?

    No.

    If you really don't want to use cooperative cancellation, there is a way to properly abort code. But it's not what I would call "simple".

    Note all the problems with aborting code above; they all have to do with corrupting the state of their application/process in some obscure or unpredictable way. The solution, then, is to isolate that uncancelable code in a separate process. Your application can then start an actual separate process and kill it when it needs to abortively cancel the code. The uncancelable code might end up corrupting the process state, but that state is about to be thrown away anyway; the uncancelable code might end up leaving resources open, but the OS is about to step in and close them anyway.

    This is the sledgehammer approach, but it works. I've had to do it a couple of times. Oh, and this is exactly how Powershell handles Ctrl-C.

    If you can, though, I'd just add a handful of ThrowIfCancellationRequested calls at some strategic points. It will take some thought, and maybe some trial-and-error, but it'll be much less work overall than a separate process.