Dreaming of multiple tasks again… with occasional exceptions

Yesterday I wrote about waiting for multiple tasks to complete. We had three asynchronous tasks running in parallel, fetching a user’s settings, reputation and recent activity. Broadly speaking, there were two approaches. First we could use TaskEx.WhenAll (which will almost certainly be folded into the Task class for release):

var settingsTask = service.GetUserSettingsAsync(userId); 
var reputationTask = service.GetReputationAsync(userId); 
var activityTask = service.GetRecentActivityAsync(userId); 

await TaskEx.WhenAll(settingsTask, reputationTask, activityTask); 

UserSettings settings = settingsTask.Result; 
int reputation = reputationTask.Result; 
RecentActivity activity = activityTask.Result;

Second we could just wait for each result in turn:

var settingsTask = service.GetUserSettingsAsync(userId);  
var reputationTask = service.GetReputationAsync(userId);  
var activityTask = service.GetRecentActivityAsync(userId);  
      
UserSettings settings = await settingsTask; 
int reputation = await reputationTask; 
RecentActivity activity = await activityTask;

These look very similar, but actually they behave differently if any of the tasks fails:

  • In the first form we will always wait for all the tasks to complete; if the settings task fails within a millisecond but the recent activity task takes 5 minutes, we’ll be waiting 5 minutes. In the second form we only wait for one at a time, so if one task fails, we won’t wait for any currently-unawaited ones to complete. (Of course if the first two tasks both succeed and the last one fails, the total waiting time will be the same either way.)
  • In the first form we should probably get to find out about the errors from all the asynchronous tasks; in the second form we only see the errors from whichever task fails first.

The second point is interesting, because in fact it looks like the CTP will throw away all but the first inner exception of an aggregated exception thrown by a Task that’s being awaited. That feels like a mistake to me, but I don’t know whether it’s by design or just due to the implementation not being finished yet. I’m pretty sure this is the same bit of code (in EndAwait for Task and Task<T>) which makes sure that we don’t get multiple levels of AggregateException wrapping the original exception as it bubbles up. Personally I’d like to at least be able to find all the errors that occurred in an asynchronous operation. Occasionally, that would be useful…

… but actually, in most cases I’d really like to just abort the whole operation as soon as any task fails. I think we’re missing a method – something like WhenAllSuccessful. If any operation is cancelled or faulted, the whole lot should end up being cancelled – with that cancellation propagating down the potential tree of async tasks involved, ideally. Now I still haven’t investigated cancellation properly, but I believe that the cancellation tokens of Parallel Extensions should make this all possible. In many cases we really need success for all of the operations – and we would like to communicate any failures back to our caller as soon as possible.

Now I believe that we could write this now – somewhat inefficiently. We could keep a collection of tasks which still haven’t completed, and wait for any of them to complete. At that point, look for all the completed ones in the set (because two could complete at the same time) and see whether any of them have faulted or been cancelled. If so, cancel the remaining operations and rethrow the exception (aka set our own task as faulted). If we ever get to the stage where all the tasks have completed – successfully – we just return so that the results can be fetched.

My guess is that this could be written more efficiently by the PFX team though. I’m actually surprised that there isn’t anything in the framework that does this. That usually means that either it’s there and I’ve missed it, or it’s not there for some terribly good reason that I’m too dim to spot. Either way, I’d really like to know.

Of course, all of this could still be implemented as extension methods on tuples of tasks, if we ever get language support for tuples. Hint hint.

Conclusion

It’s often easy to concentrate on the success path and ignore possible failure in code. Asynchronous operations make this even more of a problem, as different things could be succeeding and failing at the same time.

If you do need to write code like the second option above, consider ordering the various "await" statements so that the expected time taken in the failure case is minimized. Always consider whether you really need all the results in all cases… or whether any failure is enough to mess up your whole operation.

Oh, and if you know the reason for the lack of something like WhenAllSuccessful, please enlighten me in the comments :)

6 thoughts on “Dreaming of multiple tasks again… with occasional exceptions”

  1. How many people do you employ to write your blog posts? You’re literally writing them faster than I can read them…

  2. Maybe what i will say is totally stupid as i don’t use the task types often but it seem that the Task instances created by the compiler aren’t created with a CancellationToken, how will the hypothetical WhenAllSuccessful method cancel such tasks ?

    And as Task doesn’t expose it’s CancellationToker, how WhenAllSuccessful will know what is the token for each task even if they have one ?

  3. @virtualblackfox: Well, the cancellation token can be passed into the async method and then passed on to any tasks created by that method. So it wouldn’t be associated with the task itself, but with the child tasks. I think. Maybe. I don’t know much about cancellation yet :)

  4. As you’ve probably seen by now, something very similar to ‘WhenAllSuccessful’ is given in the Task-based Asynchronous Pattern document; it’s called WhenAllOrFirstException. (It uses a CountdownEvent to handle multiple events possibly completing at the same time.)

    The sample code in TAP doesn’t cancel the remaining tasks when one faults; it would probably be up to the caller to cancel the CancellationTokenSource(s) of the tasks it passed in to WhenAllOrFirstException if it wanted that behaviour.

    It would be nice to have standard, tested implementations of these task-based combinators available as standard operations in the framework.

  5. @Bradley: I’d missed that, thanks – I need to read the TAP more thoroughly, to be honest. Obviously there’d need to be overloads to include cancellation etc.

    I can’t see anything in the docs for Task.ContinueWith to indicate what occurs if the task has already completed… I assume the continuation just gets scheduled immediately.

Comments are closed.