Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why isn't a CancellationToken included in the Task<T> monad?

Task<T> neatly holds a "has started, might be finished" computation, which can be composed with other tasks, mapped with functions, etc. In contrast, the F# async monad holds a "could start later, might be running now" computation, along with a CancellationToken. In C#, you typically have to thread the CancellationToken through every function that works with a Task. Why did the C# team elect to wrap the computation in the Task monad, but not the CancellationToken?

like image 498
Sebastian Good Avatar asked Jun 26 '14 00:06

Sebastian Good


People also ask

What is the purpose of CancellationToken?

A CancellationToken enables cooperative cancellation between threads, thread pool work items, or Task objects. You create a cancellation token by instantiating a CancellationTokenSource object, which manages cancellation tokens retrieved from its CancellationTokenSource. Token property.

What is CancellationToken C# task?

The CancellationToken is used in asynchronous task. The CancellationTokenSource token is used to signal that the Task should cancel itself. In the above case, the operation will just end when cancellation is requested via Cancel() method.

Should I use CancellationToken?

Whether you're doing async work or not, accepting a CancellationToken as a parameter to your method is a great pattern for allowing your caller to express lost interest in the result. Supporting cancelable operations comes with a little bit of extra responsibility on your part.


1 Answers

More or less, they encapsulated the implicit use of CancellationToken for C# async methods. Consider this:

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

var task1 = new Task(() => token.ThrowIfCancellationRequested());
task1.Start();
task1.Wait(); // task in Faulted state

var task2 = new Task(() => token.ThrowIfCancellationRequested(), token);
task2.Start();
task2.Wait(); // task in Cancelled state

var task3 = (new Func<Task>(async() => token.ThrowIfCancellationRequested()))();
task3.Wait(); // task in Cancelled state

For a non-async lambda, I had to explicitly associate token with the task2 for cancellation to propagate correctly, by providing it as an argument to new Task() (or Task.Run). For an async lambda used with task3, it happens automatically as a part of async/await infrastructure code.

Moreover, any token would propagate cancellation for an async method, while for non-async computational new Task()/Task.Run lambda it has to be the same token passed to the task constructor or Task.Run.

Of course, we still have to call token.ThrowIfCancellationRequested() manually to implement the cooperative cancellation pattern. I can't answer why C# and TPL teams decided to implement it this way, but I guess they aimed to not over-complicate the syntax of async/await yet keep it flexible enough.

As to F#, I haven't looked at the generated IL code of the asynchronous workflow, illustrated in Tomas Petricek's blog post you linked. Yet, as far as I understand, the token is automatically tested only at certain locations of the workflow, those corresponding to await in C# (by analogy, we might be calling token.ThrowIfCancellationRequested() manually after every await in C#). This means that any CPU-bound work still won't be cancelled immediately. Otherwise, F# would have to emit token.ThrowIfCancellationRequested() after every IL instruction, which would be quite a substantial overhead.

like image 135
noseratio Avatar answered Oct 18 '22 21:10

noseratio