Cancellation Token in Task Constructor: Why

Cancellation token in Task constructor: why?

Passing a CancellationToken into the Task constructor associates it with the task.

Quoting Stephen Toub's answer from MSDN:

This has two primary benefits:

  1. If the token has cancellation requested prior to the Task starting to execute, the Task won't execute. Rather than transitioning to
    Running, it'll immediately transition to Canceled. This avoids the
    costs of running the task if it would just be canceled while running
    anyway.
  2. If the body of the task is also monitoring the cancellation token and throws an OperationCanceledException containing that token
    (which is what ThrowIfCancellationRequested does), then when the task
    sees that OperationCanceledException, it checks whether the OperationCanceledException's token matches the Task's
    token. If it does, that exception is viewed as an acknowledgement of
    cooperative cancellation and the Task transitions to the Canceled
    state (rather than the Faulted state).

Why can a Task be associated with a CancellationToken?

The documentation for that particular overload of Task.Run explicitly and clearly states:

A cancellation token that can be used to cancel the work if it has not yet started. Run(Action, CancellationToken) does not pass cancellationToken to action.

Tasks are not "created with" or "associated with" cancellation tokens.

So it's only used as an early out. If you give to Task.Run a token that's already cancelled, nothing is scheduled and an already cancelled task is returned. Similarly, if it becomes cancelled by the moment the task is ready to run, the resulting task will get cancelled.

Cancellation in .NET is always cooperative, not preemptive: the task's function is responsible for checking the token itself once it has begun.

What is the use of passing CancellationToken to Task Class constructor?

UPDATE:
The following msdn question describes the reason:

Passing a token into StartNew associates the token with the Task.
This has two primary benefits:

  1. If the token has cancellation
    requested prior to the Task starting to execute, the Task won't
    execute. Rather than transitioning to Running, it'll immediately
    transition to Canceled. This avoids the costs of running the task if
    it would just be canceled while running anyway.

  2. If the body of the
    task is also monitoring the cancellation token and throws an
    OperationCanceledException containing that token (which is what
    ThrowIfCancellationRequested does), then when the task sees that OCE,
    it checks whether the OCE's token matches the Task's token. If it
    does, that exception is viewed as an acknowledgement of cooperative
    cancellation and the Task transitions to the Canceled state (rather
    than the Faulted state).

Passing cancellation token to calling method VS task constructor?

The first passes a token to your method, where you can do what you want with it. The second passes the token to Task.Run that associates the task with that token.

Since cancellation in .NET is cooperative Task.Run can only cancel your task if it hadn't started executing yet (which isn't that useful) and your method can only check the token from time to time and throw if cancellation was requested but that will mark the task as faulted instead of cancelled.

For a complete solution you should actually do both:

var task = Task.Run(() => LongTask(1000000, cancellationToken), cancellationToken);

That way the task is associated with the token and you can check the token for cancellation.

Passing cancellation token to Task.Run seems to have no effect

The Task.Run has many overloads. The case of the t1 is peculiar because of the infinite while loop.

var t1 = Task.Run(() =>
{
while (true)
{
Thread.Sleep(1000);
token.ThrowIfCancellationRequested();
};
});

The compiler has to choose between these two overloads:

public static Task Run(Action action);

public static Task Run(Func<Task> function);

...and for some unknown to me reason it chooses the later. Here is the implementation of this overload:

public static Task Run(Func<Task?> function, CancellationToken cancellationToken)
{
if (function == null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.function);

// Short-circuit if we are given a pre-canceled token
if (cancellationToken.IsCancellationRequested)
return Task.FromCanceled(cancellationToken);

// Kick off initial Task, which will call the user-supplied function and yield a Task.
Task<Task?> task1 = Task<Task?>.Factory.StartNew(function, cancellationToken,
TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);

// Create a promise-style Task to be used as a proxy for the operation
// Set lookForOce == true so that unwrap logic can be on the lookout for OCEs thrown
// as faults from task1, to support in-delegate cancellation.
UnwrapPromise<VoidTaskResult> promise = new UnwrapPromise<VoidTaskResult>(task1,
lookForOce: true);

return promise;
}

The important detail is the lookForOce: true. Let's look inside the UnwrapPromise class:

// "Should we check for OperationCanceledExceptions on the outer task and interpret them
// as proxy cancellation?"
// Unwrap() sets this to false, Run() sets it to true.
private readonly bool _lookForOce;

..and at another point below:

case TaskStatus.Faulted:
List<ExceptionDispatchInfo> edis = task.GetExceptionDispatchInfos();
ExceptionDispatchInfo oceEdi;
if (lookForOce && edis.Count > 0 &&
(oceEdi = edis[0]) != null &&
oceEdi.SourceException is OperationCanceledException oce)
{
result = TrySetCanceled(oce.CancellationToken, oceEdi);
}
else
{
result = TrySetException(edis);
}
break;

So although the internally created Task<Task?> task1 ends up in a Faulted state, its unwrapped version ends up as Canceled, because the type of the exception is
OperationCanceledException (abbreviated as oce in the code).

That's a quite convoluted journey in the history of TPL, with methods introduced at different times and frameworks, in order to serve different purposes. The end result is a little bit of inconsistency, or nuanced behavior if you prefer to say it so. A relevant article that you might find interesting is this: Task.Run vs Task.Factory.StartNew by Stephen Toub.

Use a cancellation Token to cancel multiple Tasks

Use a CancellationTokenSource, in a somewhat simplified example:

CancellationTokenSource cts = new CancellationTokenSource();
async void Cancel_Click(object sender, EventArgs e)
{
cts.Cancel();
}
async void btnStart_Click(object sender, EventArgs e)
{
try{
cts = new CancellationTokenSource();
var token = cts.Token;
var t1 = Task.Run(() => Start(token));
var t2 = Task.Run(() => Start(token));
await Task.WhenAny(new []{t1, t2});
}
catch(OperationCancelledException){
// Handle canceled
}
catch(Exception){
// Handle other exceptions
}
}

void Start(CancellationToken token)
{
for (int i = 0; i < 100; i++)
{
token.ThrowIfCancellationRequested();
// Do work
Thread.Sleep(100);
}
}

When the button is clicked it will first create a new cancellationTokenSource, then start two tasks on background threads, both using the token from the newly created token source. When the cancelButton is pressed the token will be set into canceled-state, and the next time each background thread calls ThrowIfCancellationRequested they will throw an exception. This will put the task into a canceled-state, and awaiting this will throw an operationCancelledException that need to be caught. Note that when awaiting multiple tasks you might get an aggregateException that wraps multiple exceptions and need to be unpacked.

Can cancellation token be used at tasks method within?

Yes, you can easily pass the same token onto StartSomething and exceptions from it will bubble up to Control and cancel the task. If you don't then it will keep running even if the CancellationTokenwas cancelled until it returns control toControl` that observes the token:

void StartSomething(CancellationToken token)
{
while (true)
{
token.ThrowIfCancellationRequested(); // Will cancel the task.
// ...
}
}

Keep in mind though that token.ThrowIfCancellationRequested() will raise exception and the task will be canceled while !token.IsCancellationRequested will simply complete the task without marking it as canceled.

Task.Run and CancellationToken. How does cancellation work in this scenario?

It seems that if the compiler can know at compile time that the task will always throw OperationCanceledException is will end in Canceled state, otherwize it will end in Faulted.

void Main()
{
CancellationTokenSource cts = new CancellationTokenSource(1);
cts.Cancel();
var token = cts.Token;
var task = Task.Run(() =>
{
//if(GetTrue()) //Faulted
if(true) //Canceled
throw new OperationCanceledException();
});

Thread.Sleep(50);
Console.WriteLine(task.Status);
}

bool GetTrue()
{
return true;
}

But if you send in the same token to the Task.Run as you throw inside the task it will end in Canceled.

void Main()
{
CancellationTokenSource cts = new CancellationTokenSource(10000);
var token = cts.Token;
var task = Task.Run(() =>
{
cts.Cancel();
token.ThrowIfCancellationRequested();
}, token);

Thread.Sleep(50);
Console.WriteLine(task.Status);
}


Related Topics



Leave a reply



Submit