TL;DR: Do not use
I actually debated quite a bit on how to start this series! I finally decided on starting with the constructors, even though the
Task constructors are a red herring.
(not actually a red herring)
Task type has a whopping eight constructors:
The BCL avoids default parameters because they don’t work well with versioning and reflection. However, I’m going to rewrite some of the members with optional parameters to reduce the number of overloads that I need to talk about.
I’m going to call the eight constructors “actual members” because they actually exist. However, these can be reduced to only one “logical member”:
Task<T> type has eight actual constructors:
Which simplify down to a single logical constructor:
So, we have 16 actual constructors and two logical constructors.
The use case for the task constructors is extremely small.
Remember that there are two kinds of tasks: Promise Tasks and Delegate Tasks. The task constructors cannot create Promise Tasks; they only create Delegate Tasks.
Task constructors should not be used with
async, and they should only rarely be used with parallel programming.
Parallel programming can be split into two types: data parallelism and task parallelism, with the majority of parallel situations calling for data parallelism. Task parallelism can be further split into two types: static task parallelism (where the number of work items is known at the beginning of the parallel processing) and dynamic task parallelism (where the number of work items changes while they are being processed). The Parallel class and PLINQ types in the Task Parallel Library provide higher-level constructs for dealing with data parallelism and static task parallelism. The only reason you should ever create a Delegate Task for parallel code is if you are doing dynamic task parallelism. But even then, you almost never want to use the task constructors! The task constructors create a task that is not ready to run; it has to be scheduled first. This is almost never necessary; in the real world, most tasks should be scheduled immediately. The only reason you’d want to create a task and then not schedule it is if you wanted to allow the caller to determine which thread the task actually runs on. And even in that scenario, I’d recommend using
Func<Task> instead of returning an unscheduled task.
Let me put that another way: if you are doing dynamic task parallelism and need to construct a task that can run on any thread, and leave that scheduling decision up to another part of the code, and for whatever reason cannot use
Func<Task> instead, then (and only then) you should use a task constructor. I’ve written countless asynchronous and parallel applications, and I have never been in this situation.
Even shorter version: Do Not Use!
If you’re writing
async code, the easiest way to create a Promise Task is to use the
async keyword. If you’re wrapping another asynchronous API or event, use
TaskCompletionSource<T>. If you need to run some CPU-bound code and treat it asynchronously, use
Task.Run. We’ll look at all of these options and more in future posts.
If you’re writing parallel code, first try to use Parallel or PLINQ. If you actually are doing dynamic task parallelism, use
Task.Factory.StartNew. We’ll consider these options as well in future posts.
Sorry that the first post just boiled down to “don’t use this”, but it is what it is. I’ll cover all the constructor arguments such as
CancellationToken later when I cover