A Tour of Task, Part 5: Waiting

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 Task.Run).

Wait

There are five overloads of Wait:

void Wait();
void Wait(CancellationToken);
bool Wait(int);
bool Wait(TimeSpan);
bool Wait(int, CancellationToken);

These nicely simplify down to a single logical method:

void Wait() { Wait(-1); }
void Wait(CancellationToken token) { Wait(-1, token); }
bool Wait(int timeout) { return Wait(timeout, CancellationToken.None); }
bool Wait(TimeSpan timeout) { return Wait(timeout.TotalMilliseconds); }
bool Wait(int, CancellationToken);

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:

static void WaitAll(params Task[]);
static void WaitAll(Task[], CancellationToken);
static bool WaitAll(Task[], int);
static bool WaitAll(Task[], TimeSpan);
static bool WaitAll(Task[], int, CancellationToken);

Again, these nicely simplify down to a single logical method:

static void WaitAll(params Task[] tasks) { WaitAll(tasks, -1); }
static void WaitAll(Task[] tasks, CancellationToken token) { WaitAll(tasks, -1, token); }
static bool WaitAll(Task[] tasks, int timeout) { return WaitAll(tasks, timeout, CancellationToken.None); }
static bool WaitAll(Task[] tasks, TimeSpan timeout) { return WaitAll(tasks, timeout.TotalMilliseconds); }
static bool WaitAll(Task[], int, CancellationToken);

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:

static int WaitAny(params Task[]);
static int WaitAny(Task[], CancellationToken);
static int WaitAny(Task[], int);
static int WaitAny(Task[], TimeSpan);
static int WaitAny(Task[], int, CancellationToken);

Which simplify down to a single logical method:

static int WaitAny(params Task[] tasks) { return WaitAny(tasks, -1); }
static int WaitAny(Task[] tasks, CancellationToken token) { return WaitAny(tasks, -1, token); }
static int WaitAny(Task[] tasks, int timeout) { return WaitAny(tasks, timeout, CancellationToken.None); }
static int WaitAny(Task[] tasks, TimeSpan timeout) { return WaitAny(tasks, timeout.TotalMilliseconds); }
static int WaitAny(Task[], int, CancellationToken);

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:

WaitHandle IAsyncResult.AsyncWaitHandle { get; }

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.