2

I have a series of Tasks in an array. If a Task is "Good" it returns a string. If it's "Bad": it return a null.

I want to be able to run all the Tasks in parallel, and once the first one comes back that is "Good", then cancel the others and get the "Good" result.

I am doing this now, but the problem is that all the tasks need to run, then I loop through them looking for the first good result.

List<Task<string>> tasks = new List<Task<string>>();
Task.WaitAll(tasks.ToArray());
Mrinal Kamboj
  • 11,300
  • 5
  • 40
  • 74
Ian Vink
  • 66,960
  • 104
  • 341
  • 555
  • Doesn't Task.WhenAny Work for you? – Poul Bak Aug 30 '18 at 19:23
  • The problem is that I need the first Task that is good and got the data, many of the Tasks will return but failed to get what I need – Ian Vink Aug 30 '18 at 19:26
  • 3
    Does [this question](https://stackoverflow.com/questions/37528738/is-there-default-way-to-get-first-task-that-finished-successfully) do what you want? – Chris Aug 30 '18 at 19:36

4 Answers4

3

I want to be able to run all the Tasks in parallel, and once the first one comes back that is "Good", then cancel the others and get the "Good" result.

This is misunderstanding, since Cancellation in TPL is co-operative, so once the Task is started, there's no way to Cancel it. CancellationToken can work before Task is started or later to throw an exception, if Cancellation is requested, which is meant to initiate and take necessary action, like throw custom exception from the logic

Check the following query, it has many interesting answers listed, but none of them Cancel. Following is also a possible option:

public static class TaskExtension<T>
{
  public static async Task<T> FirstSuccess(IEnumerable<Task<T>> tasks, T goodResult)

    {
        // Create a List<Task<T>>
        var taskList = new List<Task<T>>(tasks);
        // Placeholder for the First Completed Task
        Task<T> firstCompleted = default(Task<T>);
        // Looping till the Tasks are available in the List
        while (taskList.Count > 0)
        {
            // Fetch first completed Task
            var currentCompleted = await Task.WhenAny(taskList);

            // Compare Condition
            if (currentCompleted.Status == TaskStatus.RanToCompletion
                && currentCompleted.Result.Equals(goodResult))
            {
                // Assign Task and Clear List
                firstCompleted = currentCompleted;
                break;
            }
            else
               // Remove the Current Task
               taskList.Remove(currentCompleted);
        }
        return (firstCompleted != default(Task<T>)) ? firstCompleted.Result : default(T);
    }
}

Usage:

var t1 = new Task<string>(()=>"bad");

var t2 = new Task<string>(()=>"bad");

var t3 = new Task<string>(()=>"good");

var t4 = new Task<string>(()=>"good");

var taskArray = new []{t1,t2,t3,t4};

foreach(var tt in taskArray)
  tt.Start();

var finalTask = TaskExtension<string>.FirstSuccess(taskArray,"good");

Console.WriteLine(finalTask.Result);

You may even return Task<Task<T>>, instead of Task<T> for necessary logical processing

Scott Chamberlain
  • 124,994
  • 33
  • 282
  • 431
Mrinal Kamboj
  • 11,300
  • 5
  • 40
  • 74
  • The `taskList.Clear();` is an interesting choice, why not a simple `break` to leave the loop? Or even better, directly call `return firstCompleted.Result`, this way you can replace the last line by an unconditional `return default(T)` – Kevin Gosse Sep 01 '18 at 13:22
  • @KevinGosse Indeed `break` is a more logical choice, for some strange reason I was more keen to empty the collection. Edited the code, thanks for the comment. – Mrinal Kamboj Sep 02 '18 at 01:43
  • Should you call .Result on the remaining tasks (after "Good") so that exceptions in the ignored tasks would not break the application? – Ivan Akcheurov Jan 03 '19 at 17:04
1

You can achieve your desired results using following example.

List<Task<string>> tasks = new List<Task<string>>();  

// ***Use ToList to execute the query and start the tasks.   
List<Task<string>> goodBadTasks = tasks.ToList();  

// ***Add a loop to process the tasks one at a time until none remain.  
while (goodBadTasks.Count > 0)  
{  
    // Identify the first task that completes.  
    Task<string> firstFinishedTask = await Task.WhenAny(goodBadTasks);  

    // ***Remove the selected task from the list so that you don't  
    // process it more than once.  
    goodBadTasks.Remove(firstFinishedTask);  

    // Await the completed task.  
    string firstFinishedTaskResult = await firstFinishedTask;  
    if(firstFinishedTaskResult.Equals("good")
         // do something

}  

EDIT : If you want to terminate all the tasks you can use CancellationToken.

For more detail read the docs.

Muhammad Hannan
  • 2,389
  • 19
  • 28
1

I was looking into Task.WhenAny() which will trigger on the first "completed" task. Unfortunately, a completed task in this sense is basically anything... even an exception is considered "completed". As far as I can tell there is no other way to check for what you call a "good" value.

While I don't believe there is a satisfactory answer for your question I think there may be an alternative solution to your problem. Consider using Parallel.ForEach.

        Parallel.ForEach(tasks, (task, state) =>
        {
            if (task.Result != null)
                state.Stop();
        });

The state.Stop() will cease the execution of the Parallel loop when it finds a non-null result.

Besides having the ability to cease execution when it finds a "good" value, it will perform better under many (but not all) scenarios.

Sailing Judo
  • 11,083
  • 20
  • 66
  • 97
  • Issue with the `Parallel Foreach` would be that it doesn't guarantee execution of all `Enumerable` elements in parallel, it has internal scheduling mechanism to induce parallelism based on multiple factors including system configuration and perceived duration of Task, therefore various elements of the IEnumerable supplied may as well run using same thread pool thread. Your answer will help in finding the first Task with good return value, but may not be the fastest. – Mrinal Kamboj Sep 01 '18 at 08:32
  • Also in case of `Parallel ForEach` is supplied with array of `Task` type, which normally not the case, then they would anyway run on separate thread pool thread, and `ParallelLoopState Stop` may not be able to stop that executing, beside the point that I am not sure of the implication of stopping the thread mid way, if this call really does it – Mrinal Kamboj Sep 01 '18 at 08:35
  • I agree with both your comments. But the question had specific goals in mind and I don't see how he can achieve those with Task.WaitAny() or Task.WhenAny(). Therefore I offered an alternative answer that may (or may not) achieve his objective. – Sailing Judo Sep 04 '18 at 15:20
0

Use Task.WhenAny It returns the finished Task. Check if it's null. If it is, remove it from the List and call Task.WhenAny Again.

If it's good, Cancel all Tasks in the List (they should all have a CancellationTokenSource.Token.

Edit:

All Tasks should use the same CancellationTokenSource.Token. Then you only need to cancel once. Here is some code to clarify:

private async void button1_Click(object sender, EventArgs e)
{
    CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();

    List<Task<string>> tasks = new List<Task<string>>();
    tasks.Add(Task.Run<string>(() => // run your tasks
       {
           while (true)
           {
               if (cancellationTokenSource.Token.IsCancellationRequested)
               {
                   return null;
               }
               return "Result";  //string or null
           }
       }));
    while (tasks.Count > 0)
    {
        Task<string> resultTask = await Task.WhenAny(tasks);
        string result = await resultTask;
        if (result == null)
        {
            tasks.Remove(resultTask);
        }
        else
        {
            // success
            cancellationTokenSource.Cancel(); // will cancel all tasks
        }
    }
}
Poul Bak
  • 10,450
  • 5
  • 32
  • 57
  • Cancellation token is cooperative, would not help until and unless explicitly used – Mrinal Kamboj Aug 30 '18 at 19:58
  • There's nothing magical about cancellation (just an object), at least when you stay away from linked ones. – Poul Bak Aug 30 '18 at 23:45
  • `CancellationToken` is not just another object, otherwise you don't need it, can work with any random object, it is a complete mechanism to induce Cooperative Cancellation for the Tasks – Mrinal Kamboj Aug 31 '18 at 09:07
  • In your current code cancellationTokenSource is not associated with any of the Task to carry out the Cancellation. – Mrinal Kamboj Aug 31 '18 at 10:17
  • Downvote all you like, but the code will Work. You can add additonal checks to ensure the task didn't end in an exception. – Poul Bak Aug 31 '18 at 16:22
  • Your Answer is incorrect and I have explained you Why ? Usage of `CancellationTokenSource` is incorrect. Just because its working for a specific use case doesn't makes it correct. Underlying assumption related to Task cancellation is incorrect. – Mrinal Kamboj Sep 01 '18 at 08:20