Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Await list of async predicates, but drop out on first false

Imagine the following class:

public class Checker
{
   public async Task<bool> Check() { ... }
}

Now, imagine a list of instances of this class:

IEnumerable<Checker> checkers = ...

Now I want to control that every instance will return true:

checkers.All(c => c.Check());

Now, this won't compile, since Check() returns a Task<bool> not a bool.

So my question is: How can I best enumerate the list of checkers? And how can I shortcut the enumeration as soon as a checker returns false? (something I presume All( ) does already)

like image 293
Vegar Avatar asked Jun 26 '14 22:06

Vegar


People also ask

Does async void wait?

For methods other than event handlers that don't return a value, you should return a Task instead, because an async method that returns void can't be awaited. Any caller of such a method must continue to completion without waiting for the called async method to finish.

Does await stop the main thread?

Because await is only valid inside async functions and modules, which themselves are asynchronous and return promises, the await expression never blocks the main thread and only defers execution of code that actually depends on the result, i.e. anything after the await expression.

What happens when async method is not awaited?

The call to the async method starts an asynchronous task. However, because no Await operator is applied, the program continues without waiting for the task to complete. In most cases, that behavior isn't expected.

Does async await run on separate thread?

An await expression in an async method doesn't block the current thread while the awaited task is running. Instead, the expression signs up the rest of the method as a continuation and returns control to the caller of the async method. The async and await keywords don't cause additional threads to be created.


2 Answers

"Asynchronous sequences" can always cause some confusion. For example, it's not clear whether your desired semantics are:

  1. Start all checks simultaneously, and evaluate them as they complete.
  2. Start the checks one at a time, evaluating them in sequence order.

There's a third possibility (start all checks simultaneously, and evaluate them in sequence order), but that would be silly in this scenario.

I recommend using Rx for asynchronous sequences. It gives you a lot of options, and it a bit hard to learn, but it also forces you to think about exactly what you want.

The following code will start all checks simultaneously and evaluate them as they complete:

IObservable<bool> result = checkers.ToObservable()
    .SelectMany(c => c.Check()).All(b => b);

It first converts the sequence of checkers to an observable, calls Check on them all, and checks whether they are all true. The first Check that completes with a false value will cause result to produce a false value.

In contrast, the following code will start the checks one at a time, evaluating them in sequence order:

IObservable<bool> result = checkers.Select(c => c.Check().ToObservable())
    .Concat().All(b => b);

It first converts the sequence of checkers to a sequence of observables, and then concatenates those sequences (which starts them one at a time).

If you do not wish to use observables much and don't want to mess with subscriptions, you can await them directly. E.g., to call Check on all checkers and evaluate the results as they complete:

bool all = await checkers.ToObservable().SelectMany(c => c.Check()).All(b => b);
like image 102
Stephen Cleary Avatar answered Nov 10 '22 00:11

Stephen Cleary


And how can I shortcut the enumeration as soon as a checker returns false?

This will check the tasks' result in order of completion. So if task #5 is the first to complete, and returns false, the method returns false immediately, regardless of the other tasks. Slower tasks (#1, #2, etc) would never be checked.

public static async Task<bool> AllAsync(this IEnumerable<Task<bool>> source)
{
    var tasks = source.ToList();

    while(tasks.Count != 0)
    {
        var finishedTask = await Task.WhenAny(tasks);

        if(! finishedTask.Result)
            return false;

        tasks.Remove(finishedTask);
    }

    return true;
}

Usage:

bool result = await checkers.Select(c => c.Check())
                            .AllAsync();
like image 35
dcastro Avatar answered Nov 09 '22 22:11

dcastro