Anthony S.
Anthony S.

Reputation: 128

How to handle tasks that are canceled without triggering a cancellation itself?

I'm trying to get tasks in C# to work for a specific use case but I'm not understanding how task continuation options affect the flow of tasks.

What I'm trying to do is get a series of tasks chained together with ContinueWith. This will look something like this:

A -> B -> C -> D

However, I want to include the option to short-circuit this in the event an error, so it should look like this:

A -> B -> C -> D -> X

So I put "OnlyOnRanToCompletion" as the task continuation option for each of the ContinueWith functions. Then, to catch the cancellation and return an error, I put a final task at the end of the chain with the task continuation option set to "OnlyOnCanceled".

The problem is that when this last block is hit, the continuation option is not met and the task then gets set to cancelled even if the original series of tasks was never cancelled.

What I want to happen is have A through D run, and if one of them results in a cancellation, skip the rest and run X. If A through D complete, the task shouldn't cancel. The solution needs to support an arbitrary number of continuations and will be created using LINQ.Expressions, so using async/await is probably not going to fly unless it's done creatively.

Some sample code that exhibits this is:

var cts = new CancellationTokenSource();
var token = cts.Token;

var t = Task.FromResult(1)
  .ContinueWith(
    x => x.Result + 1,
    token,
    TaskContinuationOptions.OnlyOnRanToCompletion,
    TaskScheduler.Default)
  .ContinueWith(
    x => x.Result + 1,
    token,
    TaskContinuationOptions.OnlyOnRanToCompletion,
    TaskScheduler.Default)
  .ContinueWith(
    x => -1,
    token,
    TaskContinuationOptions.OnlyOnCanceled,
    TaskScheduler.Default);

The expected behavior here would be to return 3, and the status not completed.

The actual result is that the task is cancelled.

How do I do this?

Also, I can't use async because my goal is to piggyback off TPL inside of something compiled from LINQ.Expressions so that it can evaluate asynchronously and handle errors at the end without throwing any exceptions.

Upvotes: 0

Views: 305

Answers (2)

Anthony S.
Anthony S.

Reputation: 128

Figured it out - to get the last continuation to run regardless of whether or not the previous continuations completed and without setting the status to canceled do this:

  1. Change the continuation option of the last continuation to TaskContinuation.None so that it always runs, so it won't cancel if it gets here with a status of completed.

  2. Don't pass in a cancellation token to the last continuation because passing in a cancellation token that has been cancelled seems to have the effect of causing the continuation to cancel if it would have otherwise run without the token.

Upvotes: 1

Servy
Servy

Reputation: 203802

See the remarks for ContinueWith for an explanation of this behavior:

The returned Task will not be scheduled for execution until the current task has completed. If the criteria specified through the continuationOptions parameter are not met, the continuation task will be canceled instead of scheduled.

Since the criteria for your last ContinueWith call weren't met, the Task returned from that call was cancelled.

Upvotes: 0

Related Questions