【问题标题】:How to implement Task.WhenAny() with a predicate如何使用谓词实现 Task.WhenAny()
【发布时间】:2016-07-10 06:11:57
【问题描述】:

我想同时执行几个异步任务。每个任务都会运行一个可以成功完成或抛出异常的 HTTP 请求。我需要await 直到第一个任务成功完成,或者直到所有任务都失败。

如何实现接受谓词的Task.WhenAny 方法的重载,以便排除未成功完成的任务?

【问题讨论】:

    标签: c# asynchronous async-await task-parallel-library


    【解决方案1】:

    等待任何任务,如果条件满足则返回任务。否则再次等待其他任务,直到没有其他任务等待。

    public static async Task<Task> WhenAny( IEnumerable<Task> tasks, Predicate<Task> condition )
    {
        var tasklist = tasks.ToList();
        while ( tasklist.Count > 0 )
        {
            var task = await Task.WhenAny( tasklist );
            if ( condition( task ) )
                return task;
            tasklist.Remove( task );
        }
        return null;
    }
    

    简单检查

    var tasks = new List<Task> {
        Task.FromException( new Exception() ),
        Task.FromException( new Exception() ),
        Task.FromException( new Exception() ),
        Task.CompletedTask, };
    
    var completedTask = WhenAny( tasks, t => t.Status == TaskStatus.RanToCompletion ).Result;
    
    if ( tasks.IndexOf( completedTask ) != 3 )
        throw new Exception( "not expected" );
    

    【讨论】:

      【解决方案2】:
      public static Task<Task<T>> WhenFirst<T>(IEnumerable<Task<T>> tasks, Func<Task<T>, bool> predicate)
      {
          if (tasks == null) throw new ArgumentNullException(nameof(tasks));
          if (predicate == null) throw new ArgumentNullException(nameof(predicate));
      
          var tasksArray = (tasks as IReadOnlyList<Task<T>>) ?? tasks.ToArray();
          if (tasksArray.Count == 0) throw new ArgumentException("Empty task list", nameof(tasks));
          if (tasksArray.Any(t => t == null)) throw new ArgumentException("Tasks contains a null reference", nameof(tasks));
      
          var tcs = new TaskCompletionSource<Task<T>>();
          var count = tasksArray.Count;
      
          Action<Task<T>> continuation = t =>
              {
                  if (predicate(t))
                  {
                      tcs.TrySetResult(t);
                  }
                  if (Interlocked.Decrement(ref count) == 0)
                  {
                      tcs.TrySetResult(null);
                  }
              };
      
          foreach (var task in tasksArray)
          {
              task.ContinueWith(continuation);
          }
      
          return tcs.Task;
      }
      

      示例用法:

      var task = await WhenFirst(tasks, t => t.Status == TaskStatus.RanToCompletion);
      
      if (task != null)
          var value = await task;
      

      请注意,这不会传播失败任务的异常(就像 WhenAny 不会传播一样)。

      您还可以为非通用 Task 创建一个此版本。

      【讨论】:

        【解决方案3】:
        public static Task<T> GetFirstResult<T>(
            ICollection<Func<CancellationToken, Task<T>>> taskFactories, 
            Predicate<T> predicate) where T : class
        {
            var tcs = new TaskCompletionSource<T>();
            var cts = new CancellationTokenSource();
        
            int completedCount = 0;
            // in case you have a lot of tasks you might need to throttle them 
            //(e.g. so you don't try to send 99999999 requests at the same time)
            // see: http://stackoverflow.com/a/25877042/67824
            foreach (var taskFactory in taskFactories)
            {
                taskFactory(cts.Token).ContinueWith(t => 
                {
                    if (t.Exception != null)
                    {
                        Console.WriteLine($"Task completed with exception: {t.Exception}");
                    }
                    else if (predicate(t.Result))
                    {
                        cts.Cancel();
                        tcs.TrySetResult(t.Result);
                    }
        
                    if (Interlocked.Increment(ref completedCount) == taskFactories.Count)
                    {
                        tcs.SetException(new InvalidOperationException("All tasks failed"));
                    }
        
                }, cts.Token);
            }
        
            return tcs.Task;
        }
        

        示例用法:

        using System.Net.Http;
        var client = new HttpClient();
        var response = await GetFirstResult(
            new Func<CancellationToken, Task<HttpResponseMessage>>[] 
            {
                ct => client.GetAsync("http://microsoft123456.com", ct),
                ct => client.GetAsync("http://microsoft123456.com", ct),
                ct => client.GetAsync("http://microsoft123456.com", ct),
                ct => client.GetAsync("http://microsoft123456.com", ct),
                ct => client.GetAsync("http://microsoft123456.com", ct),
                ct => client.GetAsync("http://microsoft123456.com", ct),
                ct => client.GetAsync("http://microsoft123456.com", ct),
                ct => client.GetAsync("http://microsoft.com", ct),
                ct => client.GetAsync("http://microsoft123456.com", ct),
                ct => client.GetAsync("http://microsoft123456.com", ct),
            }, 
            rm => rm.IsSuccessStatusCode);
        Console.WriteLine($"Successful response: {response}");
        

        【讨论】:

        • 非常感谢你们的快速响应和出色的工程解决方案。你帮了我很多。
        • 我只是门生 - 你真的应该感谢(并赞成!)我的导师 stackoverflow.com/a/38289396/67824 ;)
        【解决方案4】:

        这是对优秀 Eli Arbel 的answer 的尝试改进。这些是改进点:

        1. predicate 中的异常作为返回任务的故障传播。
        2. 在任务被接受为结果后不会调用predicate
        3. predicate 在原来的SynchronizationContext 中执行。这使得访问 UI 元素成为可能(如果从 UI 线程调用 WhenFirst 方法)
        4. 直接枚举源IEnumerable&lt;Task&lt;T&gt;&gt;,无需先转换为数组。
        public static Task<Task<T>> WhenFirst<T>(IEnumerable<Task<T>> tasks,
            Func<Task<T>, bool> predicate)
        {
            if (tasks == null) throw new ArgumentNullException(nameof(tasks));
            if (predicate == null) throw new ArgumentNullException(nameof(predicate));
        
            var tcs = new TaskCompletionSource<Task<T>>(
                TaskCreationOptions.RunContinuationsAsynchronously);
            var pendingCount = 1; // The initial 1 represents the enumeration itself
            foreach (var task in tasks)
            {
                if (task == null) throw new ArgumentException($"The {nameof(tasks)}" +
                    " argument included a null value.", nameof(tasks));
                Interlocked.Increment(ref pendingCount);
                HandleTaskCompletion(task);
            }
            if (Interlocked.Decrement(ref pendingCount) == 0) tcs.TrySetResult(null);
            return tcs.Task;
        
            async void HandleTaskCompletion(Task<T> task)
            {
                try
                {
                    await task; // Continue on the captured context
                }
                catch { } // Ignore exception
        
                if (tcs.Task.IsCompleted) return;
        
                try
                {
                    if (predicate(task))
                        tcs.TrySetResult(task);
                    else
                        if (Interlocked.Decrement(ref pendingCount) == 0)
                            tcs.TrySetResult(null);
                }
                catch (Exception ex)
                {
                    tcs.TrySetException(ex);
                }
            }
        }
        

        【讨论】:

          【解决方案5】:

          这样做的另一种方式,与Sir Rufo's answer 非常相似,但使用AsyncEnumerableIx.NET

          实现一个小助手方法,以便在任何任务完成后立即对其进行流式传输:

          static IAsyncEnumerable<Task<T>> WhenCompleted<T>(IEnumerable<Task<T>> source) =>
              AsyncEnumerable.Create(_ =>
              {
                  var tasks = source.ToList();
                  Task<T> current = null;
                  return AsyncEnumerator.Create(
                      async () => tasks.Any() && tasks.Remove(current = await Task.WhenAny(tasks)), 
                      () => current,
                      async () => { });
              });
          }
          

          然后可以按完成顺序处理任务,例如根据要求返回第一个匹配的:

          await WhenCompleted(tasks).FirstOrDefault(t => t.Status == TaskStatus.RanToCompletion)
          

          【讨论】:

          • 这个AsyncEnumerable类在哪里定义了CreateEnumerable/CreateEnumerator方法?
          • 在 Nuget 包 System.Interactive.Async 中,编辑了我的答案以提及这一点。还意识到我使用的是 3.1.1 版,updated that 使用的是 4.1.1 版
          【解决方案6】:

          只是想补充一些使用 List.Remove 的 @Peebo 和 @SirRufo 的答案(因为我还不能发表评论)

          我会考虑使用:

          var tasks = source.ToHashSet();
          

          代替:

          var tasks = source.ToList();
          

          所以删除会更有效

          【讨论】:

          • while+Task.WhenAny 方法是一种反模式:它效率低且不能很好地扩展。尝试通过用HashSet 替换List 来优化它不太可能产生任何明显的效果,因为发生了太多其他事情(每个循环上的所有剩余任务都附加和分离了延续,分配、复制了防御性数组并销毁等)。优化此模式的最佳方法是不使用它。
          • @TheodorZoulias 是的,它确实看起来不是这里最有效的答案,但它以某种方式成为最受欢迎的答案,可能是因为简单/可读性所以只想对此添加一点评论,TaskCompletionSource似乎是要走的路,谢谢你的澄清:)
          • Rom 是的,简单无疑是一种资产,如果您要处理大约 10 个任务,则性能影响可以忽略不计。但是,如果您的任务数约为 10,000,天哪!
          • 发现了一篇关于差异的很酷的文章,如果其他人想深入了解:link
          • Rom 确实,that's 读起来不错,但现在有更好的技术可用于此类事情(按完成顺序流式传输异步结果)。比如IAsyncEnumerable&lt;T&gt;s 和the channels。无需预先分配那么多TaskCompletionSource&lt;Task&lt;T&gt;&gt;s!
          猜你喜欢
          • 2017-10-01
          • 1970-01-01
          • 1970-01-01
          • 2013-04-06
          • 1970-01-01
          • 2013-07-24
          • 2021-02-06
          • 1970-01-01
          • 2014-01-26
          相关资源
          最近更新 更多