6

In his PluralSight course Asynchronous C# 5, Jon Skeet provides this implementation for a convenience extension method called InCOmpletionOrder:

public static IEnumerable<Task<T>> InCompletionOrder<T>(this IEnumerable<Task<T>> source)
{
    var inputs = source.ToList();
    var boxes  = inputs.Select(x => new TaskCompletionSource<T>()).ToList();
    int currentIndex = -1;

    foreach (var task in inputs)
    {
        task.ContinueWith(completed =>
        {
            var nextBox = boxes[Interlocked.Increment(ref currentIndex)];
            PropagateResult(completed, nextBox);
        }, TaskContinuationOptions.ExecuteSynchronously);
    }

    return boxes.Select(box => box.Task);
}

private static void PropagateResult<T>(Task<T> completedTask,
      TaskCompletionSource<T> completionSource)
{
    switch(completedTask.Status)
    {
      case TaskStatus.Canceled:
          completionSource.TrySetCanceled();
          break;
      case TaskStatus.Faulted:
          completionSource.TrySetException(completedTask.Exception.InnerExceptions);
          break;
      case TaskStatus.RanToCompletion:
          completionSource.TrySetResult(completedTask.Result);
          break;
      default:
          throw new ArgumentException ("Task was not completed.");
    }
}

In this question, Martin Neal provides a, seemingly more elegant, implementation using yield return

public static IEnumerable<Task<T>> InCompletionOrder<T>(this IEnumerable<Task<T>> source)
{
    var tasks = source.ToList();

    while (tasks.Any())
    {
        var t = Task.WhenAny(tasks);
        yield return t.Result;
        tasks.Remove(t.Result);
    }
}

Being still somewhat new to the rigours of asynchronous programming, can anyone describe the specific concerns that might arise with Martin Neal's implementation that are properly resolved by Jon Skeet's more involved implementation

Community
  • 1
  • 1
Pieter Geerkens
  • 11,775
  • 2
  • 32
  • 52
  • @MickyD: Exactly; but I hesitate to nearly quadruple the code volume until I understand, at least in broad strokes, why. **usr** has, below, provided me with two excellent reasons to do so. – Pieter Geerkens Feb 06 '16 at 23:47
  • Agreed. I just had a problem with "seemingly more elegant". One can not always equate line count with elegance for all aspects of programming. Servy makes some good points below. :) –  Feb 07 '16 at 09:12

1 Answers1

6

The second solution has a quadratic time complexity issue. The loop runs N times and each WhenAny call adds N continuations to those tasks. Don't use that code except if you know for sure that the number of tasks is very small.

The Remove call causes quadratic time complexity as well.

Also, the second piece of code is blocking. You only get back tasks as they complete. InCompletionOrder gives you those tasks immediately and they complete later.

I would regard InCompletionOrder as a library method. Put it into a utility file and it will not cause you maintenance problems. It's behavior will/can never change. I don't see code size as an issue here.

usr
  • 168,620
  • 35
  • 240
  • 369
  • 2
    Jon's also handles excepted and cancelled tasks properly, whereas the other snippet basically only functions if all tasks complete successfully. Honsetly that, and the fact that it's blocking (basically making the return type of `IEnumerable>` essentially a lie) are *far* more important than the performance issues, so I'd lead with those two points and only mention performance as a minor point. – Servy Feb 07 '16 at 04:12
  • 1
    @Servy are you sure about the exception behavior? The second snippet does not access the result of the tasks that it returns. The other point is well noted. – usr Feb 07 '16 at 10:32