13

Lets say that i have a couple of tasks:

void Sample(IEnumerable<int> someInts)
{
    var taskList = someInts.Select(x => DownloadSomeString(x));
}

async Task<string> DownloadSomeString(int x) {...}

I want to to get the result of first successful task. So, the basic solution is to write something like:

var taskList = someInts.Select(x => DownloadSomeString(x));
string content = string.Empty;
Task<string> firstOne = null;
while (string.IsNullOrWhiteSpace(content)){
    try
    {
        firstOne = await Task.WhenAny(taskList);
        if (firstOne.Status != TaskStatus.RanToCompletion)
        {
            taskList = taskList.Where(x => x != firstOne);
            continue;
        }
        content = await firstOne;
    }
    catch(...){taskList = taskList.Where(x => x != firstOne);}
}

But this solution seems to run N+(N-1)+..+K tasks. Where N is someInts.Count and K is position of first successful task in tasks, so as it's rerunning all task except one that is captured by WhenAny. So, is there any way to get first task that finished successfully with running maximum of N tasks? (if successful task will be the last one)

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Sic
  • 287
  • 3
  • 7
  • It seems to me that you want to run the tasks in sequence, not in parallel, right? – Yacoub Massad May 30 '16 at 15:10
  • If you want to run them in parallel, then `var taskList = someInts.Select(x => DownloadSomeString(x)).ToList()` should work for you. – Yacoub Massad May 30 '16 at 15:11
  • If you want to run them in sequence, then a simple `for` loop should do the job. – Yacoub Massad May 30 '16 at 15:11
  • 1
    @YacoubMassad He's *already* running them in parallel just fine, he just wants to continue executing as soon as he has the first successful result, rather than waiting for the last to finish before moving on. There's no need to run them sequentially to do that; it would defeat the whole purpose of the operation. – Servy May 30 '16 at 15:43
  • @Servy, in this case, the problem that OP is facing is because of deferred execution in `.Select(x => DownloadSomeString(x))`. Each time he is using `Where` to remove one task, he is creating the tasks all over again. A simple `.ToList()` on the created list of tasks should solve the problem. – Yacoub Massad May 30 '16 at 15:48
  • @YacoubMassad That's correct. It'd still have some problems even then, but that would help a lot. – Servy May 30 '16 at 15:51

4 Answers4

15

All you need to do is create a TaskCompletionSource, add a continuation to each of your tasks, and set it when the first one finished successfully:

public static Task<T> FirstSuccessfulTask<T>(IEnumerable<Task<T>> tasks)
{
    var taskList = tasks.ToList();
    var tcs = new TaskCompletionSource<T>();
    int remainingTasks = taskList.Count;
    foreach (var task in taskList)
    {
        task.ContinueWith(t =>
            {
                if (task.Status == TaskStatus.RanToCompletion)
                    tcs.TrySetResult(t.Result);
                else
                if (Interlocked.Decrement(ref remainingTasks) == 0)
                    tcs.SetException(new AggregateException(tasks.SelectMany(t1 => t1.Exception.InnerExceptions)));
            });
    }
    return tcs.Task;
}

And a version for tasks without a result:

public static Task FirstSuccessfulTask(IEnumerable<Task> tasks)
{
    var taskList = tasks.ToList();

    var tcs = new TaskCompletionSource<bool>();

    int remainingTasks = taskList.Count;

    foreach (var task in taskList)
    {
        task.ContinueWith(t =>
        {
            if (task.Status == TaskStatus.RanToCompletion)
                tcs.TrySetResult(true);
            else
                if (Interlocked.Decrement(ref remainingTasks) == 0)
                tcs.SetException(new AggregateException(
                    tasks.SelectMany(t1 => t1.Exception.InnerExceptions)));
        });
    }

    return tcs.Task;
}
Mrinal Kamboj
  • 11,300
  • 5
  • 40
  • 74
Servy
  • 202,030
  • 26
  • 332
  • 449
  • Did you mean if(task.Status == TaskStatus.RanToCompletion) ? – bodangly May 30 '16 at 15:56
  • If you made it return a `Task>` you could simplify the error signaling. You could return some non-successful task as the result, or null. It kind of depends on what the caller wants to achieve. – usr May 30 '16 at 16:52
  • @usr There are lots of ways to set up the error handling. It all depends on what you actually want to do in the case of a problem as to what is going to be most convenient, or necessary. – Servy May 30 '16 at 17:00
  • I just posted a question related to this code. I'd love any insights you can provide: https://stackoverflow.com/q/51311321/120955 – StriplingWarrior Jul 12 '18 at 17:24
6

The problem with "the first successful task" is what to do if all tasks fail? It's a really bad idea to have a task that never completes.

I assume you'd want to propagate the last task's exception if they all fail. With that in mind, I would say something like this would be appropriate:

async Task<Task<T>> FirstSuccessfulTask(IEnumerable<Task<T>> tasks)
{
  Task<T>[] ordered = tasks.OrderByCompletion();
  for (int i = 0; i != ordered.Length; ++i)
  {
    var task = ordered[i];
    try
    {
      await task.ConfigureAwait(false);
      return task;
    }
    catch
    {
      if (i == ordered.Length - 1)
        return task;
      continue;
    }
  }
  return null; // Never reached
}

This solution builds on the OrderByCompletion extension method that is part of my AsyncEx library; alternative implementations also exist by Jon Skeet and Stephen Toub.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Wouldn't `OrderByCompletion` wait for all tasks to complete first? – Yacoub Massad May 30 '16 at 16:21
  • 1
    I used this general approach in my earlier revisions, if you look at the history of my answer. If you do order the task it's actually a lot simpler than you've shown to compute the value, but honestly it's just easier than all of that to just add the continuations by hand in this case. – Servy May 30 '16 at 16:22
  • @YacoubMassad No. That's the whole point of the method. It's entirely asynchronous and you can imagine it as simply returning the tasks in the order they will finish (even though that's technically not what's happening under the hood). – Servy May 30 '16 at 16:23
  • So `OrderByCompletion` must be returning a new set of "fake" tasks, that would later complete depending on the completion of the tasks at input. – Yacoub Massad May 30 '16 at 16:27
  • @YacoubMassad: Yes, there's a few blog posts on the subject (Jon Skeet's and Stephen Toub's, and I think I wrote one too). – Stephen Cleary May 30 '16 at 20:11
  • 1
    @Servy: Except that adding continuations by hand is dangerous. Specifically, the current code in your answer will schedule the continuations on `TaskScheduler.Current` rather than `TaskScheduler.Default`. This is one reason why I prefer to always point people to `await` rather than `ContinueWith`; even if the code is a bit more verbose, it uses more familiar constructs (`try`, etc), and it's easier to understand and harder to get wrong. – Stephen Cleary May 30 '16 at 20:14
  • Like I said, even if you *do* want to go the order and use `await` route, you can still simplify the code a lot. You can use a `foreach` instead of a `for`, throw after the loop rather than checking the count, and avoid a try with an empty catch, which is really an anti-pattern. Also, as shown in my current answer, you probably want to use an `AggregateException` to wrap *all* exceptions, rather than just picking one, if they all fail, at least in a generalized case.. – Servy May 30 '16 at 20:36
  • This answer seems to be a duplicate of [another answer](http://stackoverflow.com/a/16939278/3610458). Duplicate question, duplicate (slightly expanded) answer by same author. However, I don't see any significant difference between Jon Skeet's and Stephen Toub's solutions. – SensorSmith Jun 03 '16 at 15:48
5

As a straight forward solution is to wait for any task, check if it is in RanToCompletion state and if not, wait again for any task except the already finished one.

async Task<TResult> WaitForFirstCompleted<TResult>( IEnumerable<Task<TResult>> tasks )
{
    var taskList = new List<Task<TResult>>( tasks );
    while ( taskList.Count > 0 )
    {
        Task<TResult> firstCompleted = await Task.WhenAny( taskList ).ConfigureAwait(false);
        if ( firstCompleted.Status == TaskStatus.RanToCompletion )
        {
            return firstCompleted.Result;
        }
        taskList.Remove( firstCompleted );
    }
    throw new InvalidOperationException( "No task completed successful" );
}
Sir Rufo
  • 18,395
  • 2
  • 39
  • 73
  • @Servy All of the solutions provided here did the same (with a different approach) - wait for N running tasks and return the first successful completed task/result. This (and also the other ones) did *not* rerun the tasks as mentioned in the question. – Sir Rufo May 30 '16 at 16:53
  • ` while ( taskList.Count > 0 )` will be infinite loop, since Remove call doesn't the value of the `taskList.Count`. In your case if there's no Task running to completion, then its an infinite loop as there's loop break statement – Mrinal Kamboj Aug 31 '18 at 09:24
  • @MrinalKamboj Is it just a guess or did you tried it? – Sir Rufo Aug 31 '18 at 09:38
  • I have tried it and looked in the MS reference source that Removal is not changing the Count value, in fact my own code did not work which led to the discovery. Most of the similar code is relying on explicit counter instead of Count property – Mrinal Kamboj Aug 31 '18 at 09:45
  • @MrinalKamboj Hmm, I cannot reproduce your List.Remove/Count issue. See https://gist.github.com/SirRufo/24f446f25142427274f644d8bdd66781 and .net fiddle cannot reproduce that issue either see https://dotnetfiddle.net/DCJVDN – Sir Rufo Aug 31 '18 at 09:54
  • Please ignore my comment, it was the use case issue, which was causing the trouble and I misunderstood the behavior. I am trying few more things, will revert, if I come across something worthwhile. Thanks for providing a verifiable code – Mrinal Kamboj Aug 31 '18 at 10:20
  • On another note, in your version you will have Tasks, which are not tracked for completion, post receiving the first completed Task, which is not a good idea as per Stephen Cleary and also mentioned by Stephen Toub [here](https://blogs.msdn.microsoft.com/pfxteam/2011/10/02/dont-forget-to-complete-your-tasks/). You can consider a Cancellation Token to Cancel the remaining Tasks or complete then and then order as mentioned by Stephen Cleary – Mrinal Kamboj Aug 31 '18 at 11:00
  • @MrinalKamboj Well, I have only answered the asked question, not more – Sir Rufo Aug 31 '18 at 11:30
0

Here is a robust and efficient WhenFirstSuccessful implementation, based on a similar WhenFirst implementation that I have posted in an other question. That question is about selecting a task based on the result of a predicate. This question is about selected a task based on the mere fact that it completed successfully. So the WhenFirstSuccessful is just a special case of the WhenFirst.

public static async Task<Task<TResult>> WhenFirstSuccessful<TResult>(
    params Task<TResult>[] tasks)
{
    ArgumentNullException.ThrowIfNull(tasks);

    using CancellationTokenSource cts = new();
    Task<TResult> selectedTask = null;
    IEnumerable<Task> continuations = tasks
        .Where(task => task is not null)
        .TakeWhile(_ => !cts.IsCancellationRequested)
        .Select(task => task.ContinueWith(t =>
        {
            if (t.IsCompletedSuccessfully)
                if (Interlocked.CompareExchange(ref selectedTask, t, null) is null)
                    cts.Cancel();
        }, cts.Token, TaskContinuationOptions.DenyChildAttach |
            TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default));

    Task whenAll = Task.WhenAll(continuations);
    try { await whenAll.ConfigureAwait(false); }
    catch when (whenAll.IsCanceled) { } // Ignore
    return selectedTask;
}

You could read in my other answer the reasons that make this implementation robust, clean and efficient.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104