Recent posts have considered several members that wait for tasks to complete (
GetAwaiter().GetResult()). One common disadvantage that all of these have is that they synchronously block the calling thread while waiting for the task to complete.
Today’s post talks about continuations. A continuation is a delegate that you can attach to a task and tell the task “run this when you’re done.” When the task completes, it will then schedule its continuations. The task that a continuation attaches to is called the “antecedent” task.
Continuations are important because they don’t block any threads. Instead of (synchronously) waiting for a task to complete, a thread may just attach a continuation for the task to run whenever it does complete. This is the essence of asynchrony, and the
await system uses continuations whenever it deals with tasks.
The most low-level way to attach continuations to a task is to use its
ContinueWith method. There are quite a number of overloads, but the general idea is to attach a delegate as a continuation for the task:
Whew, that’s a lot of overloads! Let’s break it down a little. First, the overloads containing an
object parameter just pass that value through to the continuation delegate; this is just an optimization to avoid an extra allocation in some cases, so we can ignore those overloads for now:
There’s also three optional parameters: a
CancellationToken (defaulting to
CancellationToken.None), a set of
TaskContinuationOptions (defaulting to
TaskContinuationOptions.None), and a
TaskScheduler (defaulting to
TaskScheduler.Current). So this list of overloads can be further simplified to:
Task<T> type has its own matching set of overloads. I won’t bore you with the details - there are another 20 method signatures, which simplify in the same manner down to:
At this point, it should be clear that there are two primary types of continuation delegates that can be passed to
ContinueWith: one has a result value (
Func<...>) and the other does not (
Action<...>). The continuation delegate always receives a task as a parameter. This is the task that the continuation is attaching to, so if you were to call
task.ContinueWith(t => ...), then
t refer to the same antecedent task instance.
ContinueWith also returns a task. This task represents the continuation itself. So, each continuation is itself a task, and may have its own continuations, and so on.
Let’s talk a bit more about the optional parameters.
CancellationToken. If you cancel the token before the continuation is scheduled, then the continuation delegate never actually runs - it’s cancelled. However, note that the token does not cancel the continuation once it has started. In other words, the
CancellationToken cancels the scheduling of the continuation, not the continuation itself. For this reason, I think the
CancellationToken parameter is misleading, and I never use it myself.
The next parameter is
TaskContinuationOptions, a collection of options for the continuation. Most options either have to do with conditions, scheduling, or parenting for the continuation. The
None option means to use the default behavior; however, in modern applications, these defaults are only appropriate for dynamic task parallelism, which is extremely rare.
The “condition options” will only schedule the continuation if the antecedent task completes in a matching state.
OnlyOnCanceled will only schedule the continuation if the antecedent task completes in a specific state.
NotOnCanceled will only schedule the continuation if the antecedent task completes in another state. All of these “condition options” are roughly equivalent to just checking the task’s
Status from within the continuation.
Update, 2015-01-30 (suggested by Bar Arnon): If the condition option is met by the antecedent task (e.g., the task completes in a
RanToCompletion state and the continuation specified the
OnlyOnRanToCompletion option), then the continuation is scheduled normally. However, if the condition option is not met (e.g., the task completes in a faulted state but the continuation specified the
OnlyOnRanToCompletion option), then the continuation is cancelled. The continuation delegate is never executed and the continuation task immediately moves to the canceled state.
Several “scheduling options” are passed along to the
TaskScheduler that is responsible for scheduling the continuation.
PreferFairness is a hint asking for FIFO behavior.
LongRunning is a hint that the continuation will execute for a long time.
ExecuteSynchronously is a request that the continuation be scheduled on the same thread that completes the antecedent task. Note that all of these are just hints; it is entirely appropriate for the
TaskScheduler to ignore them all; in particular,
ExecuteSynchronously does not guarantee that the continuation will execute synchronously.
As of this writing, in the .NET 4.6 preview, there is another option called
RunContinuationsAsynchronously, which seems to force continuations to execute asynchronously. Currently, there is no way to absolutely force continuations to be synchronous or asynchronous; forcing asynchronous continuations would certainly be useful in some situations.
Update, 2015-02-02: The .NET team has released a post describing the
RunContinuationsAsynchronously option. As the name implies, it does in fact run the continuation asynchronously.
There are a few more “scheduling options” that are not passed to the
HideScheduler option (introduced in .NET 4.5) will use the given task scheulder to schedule the continuation, but then will pretend that there is no current task scheduler while the continuation is executing; this can be used as a workaround for the unexpected default task scheduler (described below).
LazyCancellation (introduced in .NET 4.5) is an option that ensures the continuation is completed (canceled) only after its antecedent completes. Without
LazyCancellation, if the cancellation token passed to
ContinueWith is cancelled, it could cancel the continuation before the original task even completed.
The “parenting options” control how the continuation task is attached to the antecedent task. Attached child tasks change the behavior of their parent task in ways that are convenient in some dynamic task parallelism scenarios, but are unexpected and awkward anywhere outside that (extremely small) use case.
AttachedToParent will attach the continuation as a child task of the antecedent task. In modern code, you almost never want this option; more importantly, you almost never want other code to attach child tasks to your tasks. For this reason, the
DenyChildAttach option was introduced in .NET 4.5, which prevents any continuations from using
The final optional parameter is a
TaskScheduler that is used to schedule the continuation. Unfortunately, the default value for this parameter is not
TaskScheduler.Default, but rather
TaskScheduler.Current. This fact has caused quite a bit of confusion over the years, because the vast majority of the time, developers expect (and desire)
Task.Factory.StartNew has a similar problem that I have described earlier. Since this default value is unexpected (and almost always undesirable), I recommend that you always pass a
TaskScheduler value to
ContinueWith. Many companies have run into this issue and enforce similar rules on their codebase.
In conclusion, I do not recommend using
ContinueWith at all, unless you are doing dynamic task parallelism (which is extremely rare). In modern code, you should almost always use
await instead of
ContinueWith. There are several benefits to
One benefit is when working with other asynchronous code. As mentioned above,
ContinueWith can take only a limited number of delegates, none of which are
async-aware delegates. When dealing with asynchronous continuations,
ContinueWith will treat them as though they were synchronous. This can cause some manner of confusion when working with continuations of those continuations. Also, this means the scheduling options (e.g.,
LongRunning) do not work as most developers expect; they are only applied to the initial synchronous portion of an asynchronous delegate. In contrast,
await works naturally with asynchronous continuations.
Another benefit is a better default task scheduler. Code using
ContinueWith should always explicitly specify a task scheduler to reduce confusion, but
await has much more reasonable default behavior. Modern code almost never uses task schedulers; it either uses
SynchronizationContext.Current or the thread pool scheduler.
The last benefit is that
await uses the most appropriate options by default. When you
await an incomplete task, under the hood
await does use
ContinueWith to schedule a continuation for you. However, it will automatically use the appropriate options (
ExecuteSynchronously), and doesn’t allow you to specify options that will not work correctly (e.g.,
In short, prefer
ContinueWith is useful when doing dynamic task parallelism, but in every other scenario,
await is preferred.
ContinueWhenAny is a way of executing a single continuation when any of a set of tasks completes. So, it’s a way to attach a single continuation to multiple tasks, and only have that continuation run when the first task completes.
TaskFactory type has a set of
ContinueWhenAny overloads that are somewhat similar to
Each of those groups of four overloads simplify down to a central method:
The overloads with a
TAntecedentResult generic parameter are for when the antecedent tasks all have the same result type. The overloads with a
TResult are for when the continuation returns a result of its own. The
TaskFactory<TResult> type only has overloads supporting continuations that return a result, so it has half the overloads that
The default parameter values work similarly to
ContinueWith, except that they are specified by the
TaskFactory properties. So, the default
TaskFactory.CancellationToken, the default
ContinuationOptions value is
TaskFactory.ContinuationOptions, and the default
TaskFactory.Scheduler, all of which may be set by passing the desired values into the
Note that the default
TaskScheduler is still dangerous: anytime a
TaskFactory is constructed without an explicit
TaskScheduler, it will default to the value of
TaskScheduler.Current at the time
ContinueWhenAny is called. This causes the same surprising behavior as it does for
ContinueWith. Note that the static
Task.Factory does have this problematic default task scheduler.
I recommend not using these overloads at all; instead, use
await Task.WhenAny(...) (see below) to asynchronously wait for one of a set of tasks to complete.
ContinueWhenAll is just like
ContinueWhenAny, except the logic is that the continuation is executed once after all the antecedent tasks have completed. There are sixteen overloads on
TaskFactory and eight on
TaskFactory<TResult>, exactly like
ContinueWhenAny. The same default parameter logic applies.
And the same default
TaskScheduler is still dangerous.
And I recommend not using these overloads at all, either; instead, use
await Task.WhenAll(...) (see below).
Task.WhenAll returns a task that completes when all of the antecedent tasks have completed. This is conceptually similar to
TaskFactory.ContinueWhenAll, but works much more nicely with
IEnumerable<> overloads allow you to pass in a sequence of tasks, such as a LINQ expression. The sequence is immediately reified (i.e., copied to an array). For example, this allows you to pass the results of a
Select expression directly to
WhenAll. Personally, I usually prefer to explicitly reify the sequence by calling
ToArray() so that it’s obvious that’s what’s happening, but some folks like the ability to pass the sequence directly in.
The overloads with the
TResult generic parameter will retrieve all the results of those tasks, as an array. This is very convenient when you have multiple operations of a similar nature. For example, you can do two concurrent downloads as such:
This is also powerful when combined with LINQ. The code below will simultaneously download whatever urls are in the source sequence:
Task.WhenAny is similar to
Task.WhenAll, but instead of asynchronously waiting for all antecedent tasks to complete, it asynchronously waits for only one. It has a similar set of overloads:
TResult overloads serve the same purposes as they do for
WhenAll. However, the return type of
WhenAny is interesting.
WhenAny returns a task that is completed when any of the antecedent tasks complete. The result of that task is the antecedent task that completed.
This means that applying a single
await to a call to
WhenAny will give you the task that completed. This allows you to do things like do two operations at the same time and see which finishes first:
Usually, when you use
WhenAny, you actually don’t care about the tasks that don’t complete first. That is, only the results of the first task are important. In this scenario, you can make use of the rare but legal “double await”:
If you find the “double await” confusing, just break it out and specify the types. The code above is the same as:
I do recommend using
await to retrieve the results of the completed task. In this case, it might seem that
await is supurfluous, since we know that the task is already completed. However,
await is still better than
await will not wrap exceptions inside an