1

In my application I have a List<Task<Boolean>> that I Task.Wait[..] on to determine if they completed successfully (Result = true). Though if during my waiting a Task completes and returns a falsey value I want to cancel all other Task I am waiting on and do something based on this.

I have created two "ugly" methods to do this

// Create a CancellationToken and List<Task<..>> to work with
CancellationToken myCToken = new CancellationToken();
List<Task<Boolean>> myTaskList = new List<Task<Boolean>>();

//-- Method 1 --
    // Wait for one of the Tasks to complete and get its result
Boolean finishedTaskResult = myTaskList[Task.WaitAny(myTaskList.ToArray(), myCToken)].Result;

    // Continue waiting for Tasks to complete until there are none left or one returns false
    while (myTaskList.Count > 0 && finishedTaskResult)
    {
        // Wait for the next Task to complete
        finishedTaskResult = myTaskList[Task.WaitAny(myTaskList.ToArray(), myCToken)].Result;
        if (!finishedTaskResult) break;
    }
    // Act on finishTaskResult here

// -- Method 2 -- 
    // Create a label to 
    WaitForOneCompletion:
    int completedTaskIndex = Task.WaitAny(myTaskList.ToArray(), myCToken);

    if (myTaskList[completedTaskIndex].Result)
    {
        myTaskList.RemoveAt(completedTaskIndex);
        goto WaitForOneCompletion;
    }
    else
        ;// One task has failed to completed, handle appropriately 

I was wondering if there was a cleaner way to do this, possibly with LINQ?

KDecker
  • 6,928
  • 8
  • 40
  • 81

3 Answers3

2

Jon Skeet, Stephen Toub, and myself all have variations on the "order by completion" approach.

However, I find that usually people don't need this kind of complexity, if they focus their attention a bit differently.

In this case, you have a collection of tasks, and want them canceled as soon as one of them returns false. Instead of thinking about it from a controller perspective ("how can the calling code do this"), think about it from the task perspective ("how can each task do this").

If you introduce a higher-level asynchronous operation of "do the work and then cancel if necessary", you'll find your calling code cleans up nicely:

public async Task DoWorkAndCancel(Func<CancellationToken, Task<bool>> work,
    CancellationTokenSource cts)
{
  if (!await work(cts.Token))
    cts.Cancel();
}

List<Func<CancellationToken, Task<bool>>> allWork = ...;
var cts = new CancellationTokenSource();
var tasks = allWork.Select(x => DoWorkAndCancel(x, cts));
await Task.WhenAll(tasks);
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • I like this. I am just now getting comfortable enough with `Task` that some of the high level possibilities are opening up to me. I find that I am going over TPL code I wrote a few weeks/months ago and questioning what/how I was thinking at the time. I will make this a TODO and get to it soon because this does make quite a bit of sense. – KDecker Apr 22 '16 at 17:49
1

You can use the following method to take a sequence of tasks and create a new sequence of tasks that represents the initial tasks but returned in the order that they all complete:

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

    var taskSources = new BlockingCollection<TaskCompletionSource<T>>();

    var taskSourceList = new List<TaskCompletionSource<T>>(taskList.Count);
    foreach (var task in taskList)
    {
        var newSource = new TaskCompletionSource<T>();
        taskSources.Add(newSource);
        taskSourceList.Add(newSource);

        task.ContinueWith(t =>
        {
            var source = taskSources.Take();

            if (t.IsCanceled)
                source.TrySetCanceled();
            else if (t.IsFaulted)
                source.TrySetException(t.Exception.InnerExceptions);
            else if (t.IsCompleted)
                source.TrySetResult(t.Result);
        }, CancellationToken.None, TaskContinuationOptions.PreferFairness, TaskScheduler.Default);
    }

    return taskSourceList.Select(tcs => tcs.Task);
}

Now that you have the ability to order the tasks based on their completion you can write the code basically exactly as your requirements dictate:

foreach(var task in myTaskList.Order())
    if(!await task)
        cancellationTokenSource.Cancel();
Servy
  • 202,030
  • 26
  • 332
  • 449
  • I think I understand this, I am still thinking about it, but right now it looks like each `Task` would run serially, though this seems wrong to me. – KDecker Apr 21 '16 at 17:39
  • @KDecker What makes you think that they'd execute sequentially? The whole premise of your code is that you've already started all of the tasks, so you already know that all of the tasks are running in parallel *before we even get to the code in your question*. – Servy Apr 21 '16 at 17:45
  • That's why the thought didn't make sense to me. I guess its the use of `ContinueWith` and `TaskCompletionSource` that is confusing me. I have seen both used but only sparingly and never by me. I "see" how this works but I can't explain it. I think I just need to keep looking at it. // Thanks though! – KDecker Apr 21 '16 at 17:49
  • Ahhh I think I grasp it now. Each `Task` is given a continuation, which takes a `TaskCompSource` from the `BlockingCollection` (so then they complete "in order"). Once the task gets a TCS it sets the TCS's result to its result. The method returns a `List`, each can be awaited, but the tasks are added to this list in the order they complete. // Not a pretty explanation, but I think I got it. Thanks again! – KDecker Apr 21 '16 at 18:23
  • I took the liberty of returning `IEnumerable>>` where the `int` represents the index of the `Task` in the original argument list. This way we can identify the tasks even after they are ordered! – KDecker Apr 21 '16 at 19:44
  • Why are you using `BlockingCollection` when you don't block? Wouldn't `ConcurrentQueue` make more sense? – svick Apr 22 '16 at 17:39
  • @svick BlockingCollection is going to be using a `ConcurrentQueue` under the hood, so it's virtually identical, this just has a much cleaner API. – Servy Apr 25 '16 at 13:02
  • I should point out a minor flaw of the `Order` method. In case some tasks are canceled, the original `CancellationToken` information will not be propagated when the tasks are awaited. The `OperationCanceledException` will not have the correct token in its [`CancellationToken`](https://learn.microsoft.com/en-us/dotnet/api/system.operationcanceledexception.cancellationtoken) property. Btw a better name for the method would be `OrderByCompletion`. `Order` is too generic and ambiguous. – Theodor Zoulias Nov 18 '21 at 18:08
0

Using Task.WhenAny implementation, you can create like an extension overload that receives a filter too.

This method returns a Task that will complete when any of the supplied tasks have completed and the result pass the filter.

Something like this:

static class TasksExtensions
{
    public static Task<Task<T>> WhenAny<T>(this IList<Task<T>> tasks, Func<T, bool> filter)
    {
        CompleteOnInvokePromiseFilter<T> action = new CompleteOnInvokePromiseFilter<T>(filter);

        bool flag = false;
        for (int i = 0; i < tasks.Count; i++)
        {
            Task<T> completingTask = tasks[i];

            if (!flag)
            {
                if (action.IsCompleted) flag = true;
                else if (completingTask.IsCompleted)
                {
                    action.Invoke(completingTask);
                    flag = true;
                }
                else completingTask.ContinueWith(t =>
                {
                    action.Invoke(t);
                });
            }
        }

        return action.Task;
    }
}

class CompleteOnInvokePromiseFilter<T>
{
    private int firstTaskAlreadyCompleted;
    private TaskCompletionSource<Task<T>> source;
    private Func<T, bool> filter;

    public CompleteOnInvokePromiseFilter(Func<T, bool> filter)
    {
        this.filter = filter;
        source = new TaskCompletionSource<Task<T>>();
    }

    public void Invoke(Task<T> completingTask)
    {
        if (completingTask.Status == TaskStatus.RanToCompletion && 
            filter(completingTask.Result) && 
            Interlocked.CompareExchange(ref firstTaskAlreadyCompleted, 1, 0) == 0)
        {
            source.TrySetResult(completingTask);
        }
    }

    public Task<Task<T>> Task { get { return source.Task; } }

    public bool IsCompleted { get { return source.Task.IsCompleted; } }
}

You can use this extension method like this:

List<Task<int>> tasks = new List<Task<int>>();    
...Initialize Tasks...

var task = await tasks.WhenAny(x => x % 2 == 0);

//In your case would be something like tasks.WhenAny(b => b);
Arturo Menchaca
  • 15,783
  • 1
  • 29
  • 53