Today, we’ll look at a variety of ways that code can block on a task. All of these options block the calling thread until the task completes, so they’re almost never used with Promise Tasks. Note that blocking on a Promise Task is a common cause of deadlocks; blocking is almost exclusively used with Delegate Tasks (i.e., a task returned from
There are five overloads of
These nicely simplify down to a single logical method:
Wait is rather simple: it will block the calling thread until the task completes, a timeout occurs, or the wait is cancelled. If the wait is cancelled, then
Wait raises an
OperationCanceledException. If a timeout occurs, then
false. If the task completes in a failed or canceled state, then
Wait wraps any exceptions into an
AggregateException. Note that a canceled task will raise an
OperationCanceledException wrapped in an
AggregateException, whereas a canceled wait will raise an unwrapped
Task.Wait is occasionally useful, if it’s done in the correct context. For example, the
Main method of a Console application can use
Wait if it has asynchronous work to do, but wants the main thread to synchronously block until that work is done. However, most of the time,
Task.Wait is dangerous because of its deadlock potential.
For asynchronous code, use
await instead of
The overloads for
WaitAll are very similar to the overloads of
Again, these nicely simplify down to a single logical method:
These are practically identical to
Task.Wait, except they wait for multiple tasks to all complete. Similarly to
Task.WaitAll will throw
OperationCanceledException if the wait is cancelled, or an
AggregateException if any of the tasks fail or are cancelled.
WaitAll will return
false if a timeout occurs.
Task.WaitAll should be very rarely used. It is occasionally useful when working with Delegate Tasks, but even this usage is rare. Developers writing parallel code should first attempt data parallelism; and even if task parallism is necessary, then parent/child tasks may result in cleaner code than defining ad-hoc dependencies with
Task.WaitAll (for synchronous code) is rare, but
Task.WhenAll (for asynchronous code) is common.
Task.WaitAny is similar to
WaitAll except it only waits for the first task to complete (and returns the index of that task). Again, we have the similar overloads:
Which simplify down to a single logical method:
The semantics of
WaitAny are a bit different than
WaitAny merely waits for the first task to complete. It will not propagate that task’s exception in an
AggregateException. Rather, any task failures will need to be checked after
WaitAny will return
-1 on timeout, and will throw
OperationCanceledException if the wait is cancelled.
Task.WaitAll is rarely used,
Task.WaitAny should hardly ever be used at all.
Task type actually implements
IAsyncResult for easy interoperation with the (unfortunately named) Asynchronous Programming Model (APM). This means
Task has a wait handle as one of its properties:
Note that this member is explicitly implemented, so consuming code must cast the
IAsyncResult before reading it. The actual underlying wait handle is lazy-allocated.
AsyncWaitHandle should be extremely, extremely rare. It only makes sense if you have tons of existing code that is built around
WaitHandle. If you do read the
AsyncWaitHandle property, strongly consider disposing the task instance.
There are a few corner cases where a single
Task.Wait could be useful; but in general, code should not synchronously block on a task.