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

后端 未结 6 872
孤街浪徒
孤街浪徒 2021-01-12 21:20

Imagine the following class:

public class Checker
{
   public async Task Check() { ... }
}

Now, imagine a list of instances of

相关标签:
6条回答
  • 2021-01-12 21:36

    All wasn't built with async in mind (like all LINQ), so you would need to implement that yourself:

    async Task<bool> CheckAll()
    {
        foreach(var checker in checkers)
        {
            if (!await checker.Check())
            {
                return false;
            }
        }
    
        return true;
    }
    

    You could make it more reusable with a generic extension method:

    public static async Task<bool> AllAsync<TSource>(this IEnumerable<TSource> source, Func<TSource, Task<bool>> predicate)
    {
        foreach (var item in source)
        {
            if (!await predicate(item))
            {
                return false;
            }
        }
    
        return true;
    }
    

    And use it like this:

    var result = await checkers.AllAsync(c => c.Check());
    
    0 讨论(0)
  • 2021-01-12 21:41

    "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);
    
    0 讨论(0)
  • 2021-01-12 21:42

    As a more out-of-the-box alternative, this seems to run the tasks in parallel and return shortly after the first failure:

    var allResult = checkers
        .Select(c => Task.Factory.StartNew(() => c.Check().Result))
        .AsParallel()
        .All(t => t.Result);
    

    I'm not too hot on TPL and PLINQ so feel free to tell me what's wrong with this.

    0 讨论(0)
  • 2021-01-12 21:48

    You could do

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

    but that would run the tasks synchronously, which may be very slow depending on the implementation of Check().

    0 讨论(0)
  • 2021-01-12 21:51

    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();
    
    0 讨论(0)
  • 2021-01-12 21:55

    Here's a fully functional test program, following in steps of dcastro:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace AsyncCheckerTest
    {
        public class Checker
        {
            public int Seconds { get; private set; }
    
            public Checker(int seconds)
            {
                Seconds = seconds;
            }
    
            public async Task<bool> CheckAsync()
            {
                await Task.Delay(Seconds * 1000);
                return Seconds != 3;
            }
        }
    
        class Program
        {
            static void Main(string[] args)
            {
                var task = RunAsync();
                task.Wait();
    
                Console.WriteLine("Overall result: " + task.Result);
                Console.ReadLine();
            }
    
            public static async Task<bool> RunAsync()
            {
                var checkers = new List<Checker>();
                checkers
                    .AddRange(Enumerable.Range(1, 5)
                    .Select(i => new Checker(i)));
    
                return await checkers
                                .Select(c => c.CheckAsync())
                                .AllAsync();
            }
        }
    
        public static class ExtensionMethods
        {
            public static async Task<bool> AllAsync(this IEnumerable<Task<bool>> source)
            {
                var tasks = source.ToList();
    
                while (tasks.Count != 0)
                {
                    Task<bool> finishedTask = await Task.WhenAny(tasks);
    
                    bool checkResult = finishedTask.Result;
    
                    if (!checkResult)
                    {
                        Console.WriteLine("Completed at " + DateTimeOffset.Now + "...false");
                        return false;
                    }
    
                    Console.WriteLine("Working... " + DateTimeOffset.Now);
                    tasks.Remove(finishedTask);
                }
    
                return true;
            }
        }
    }
    

    Here's sample output:

    Working... 6/27/2014 1:47:35 AM -05:00
    Working... 6/27/2014 1:47:36 AM -05:00
    Completed at 6/27/2014 1:47:37 AM -05:00...false
    Overall result: False
    

    Note that entire eval ended when exit condition was reached, without waiting for the rest to finish.

    0 讨论(0)
提交回复
热议问题