A Tour of Task, Part 5: Waiting
• CommentsToday, 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 Task.Run
).
Wait
There are five overloads of Wait
:
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 Wait
returns 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 OperationCanceledException
.
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 Task.Wait
.
WaitAll
The overloads for WaitAll
are very similar to the overloads of Wait
:
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.Wait
, 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
.
Note that Task.WaitAll
(for synchronous code) is rare, but Task.WhenAll
(for asynchronous code) is common.
WaitAny
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 WaitAll
and Wait
: 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
returns. WaitAny
will return -1
on timeout, and will throw OperationCanceledException
if the wait is cancelled.
If Task.WaitAll
is rarely used, Task.WaitAny
should hardly ever be used at all.
AsyncWaitHandle
The 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 Task
as IAsyncResult
before reading it. The actual underlying wait handle is lazy-allocated.
Code using 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.
Conclusion
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.