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
?

- 6,310
- 2
- 33
- 57
-
2In C#, you have the *"could start later, might be running now" computation, along with a CancellationToken* option in the form of `var task = new Task(action, token)`. – noseratio Jun 26 '14 at 02:41
2 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.

- 59,932
- 34
- 208
- 486
-
-
1@GregC, anytime I need to do CPU-bound work in a UI app: `var pi = await Task.Run(() => CalcPi(digits, token), token)`. – noseratio Jun 26 '14 at 05:00
-
1
-
I know what it is that most people want to do with async/await. It's meant for monadic injection of functionality, similar to IEnumerable. It makes continuation-passing style look procedural, at the expense of additional orchestration. As such, I wouldn't use it in places where you wouldn't use CPS. – GregC Jun 26 '14 at 14:51
-
2@GregC, I'd rather compare `IEnumerable` to `IObservable` (a feature of Rx, as opposed to TPL's `Task`). – noseratio Jun 26 '14 at 15:00
-
1I'd like to link an exceptional explanation by Thomas P http://stackoverflow.com/a/15615804/90475 – GregC Jun 26 '14 at 15:43
Task was initially made to contain additional features in the class, but later was changed to aggregate an object with additional feature support. All in the name of performance.
http://blogs.msdn.com/b/pfxteam/archive/2011/11/10/10235962.aspx (see "restructuring Task" in the paper) The paper by Joseph E. Hoag gives nice insights into optimizations done in .NET 4.5. I believe it would be a worthwhile read for anyone trying to squeeze the last 10% of performance out of async/await.
I assume a similar thought process was applied when deciding how to package cancellation functionality.
I can't speak for C# or BCL team, but I assume it's a performance optimization that's either only possible in F# compiler, or performance was immaterial to F# team. SRP, baby!

- 7,737
- 2
- 53
- 67
-
1I'd forgotten about that Hoag paper. Seem like the drive was to keep the Task object itself as small as possible, hence not carrying around the Cancellation token, but instead carrying it around in the state machine generated by async/await. – Sebastian Good Jun 26 '14 at 03:47