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
?
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.
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With