13

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?

Sebastian Good
  • 6,310
  • 2
  • 33
  • 57
  • 2
    In 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 Answers2

10

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.

noseratio
  • 59,932
  • 34
  • 208
  • 486
  • When and why would you want to use async/await for CPU-bound work? – GregC Jun 26 '14 at 04:57
  • 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
    @GregC As a form of parallelization. `await Task.WhenAll(tasks)`. – Aron Jun 26 '14 at 06:21
  • 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
  • 1
    I'd like to link an exceptional explanation by Thomas P http://stackoverflow.com/a/15615804/90475 – GregC Jun 26 '14 at 15:43
2

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!

GregC
  • 7,737
  • 2
  • 53
  • 67
  • 1
    I'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