是否有默认方式获得成功完成的第一个任务?

让我们说我有几个任务:

void Sample(IEnumerable someInts) { var taskList = someInts.Select(x => DownloadSomeString(x)); } async Task DownloadSomeString(int x) {...} 

我想得到第一个成功任务的结果。 所以,基本的解决方案是写下这样的东西:

 var taskList = someInts.Select(x => DownloadSomeString(x)); string content = string.Empty; Task firstOne = null; while (string.IsNullOrWhiteSpace(content)){ try { firstOne = await Task.WhenAny(taskList); if (firstOne.Status != TaskStatus.RanToCompletion) { taskList = taskList.Where(x => x != firstOne); continue; } content = await firstOne; } catch(...){taskList = taskList.Where(x => x != firstOne);} } 

但是这个解决方案似乎运行N +( N -1)+ .. + K任务。 其中NsomeInts.CountK是任务中第一个成功任务的位置,因此它重新运行除WhenAny捕获的任务之外的所有任务。 那么,有没有办法让第一个任务成功完成并运行最多N任务? (如果成功的任务将是最后一个)

“第一个成功的任务”的问题是如果所有任务都失败了怎么办? 拥有永不完成的任务是一个非常糟糕的主意 。

我假设如果它们失败,你想传播最后一个任务的exception。 考虑到这一点,我会说这样的事情是合适的:

 async Task> FirstSuccessfulTask(IEnumerable> tasks) { Task[] ordered = tasks.OrderByCompletion(); for (int i = 0; i != ordered.Length; ++i) { var task = ordered[i]; try { await task.ConfigureAwait(false); return task; } catch { if (i == ordered.Length - 1) return task; continue; } } return null; // Never reached } 

此解决方案基于OrderByCompletion扩展方法 ,该方法是我的AsyncEx库的 一部分 ; Jon Skeet和Stephen Toub也存在替代实施方案。

您需要做的就是创建一个TaskCompletionSource ,为每个任务添加一个延续,并在第一个任务成功完成时设置它:

 public static Task FirstSuccessfulTask(IEnumerable> tasks) { var taskList = tasks.ToList(); var tcs = new TaskCompletionSource(); int remainingTasks = taskList.Count; foreach (var task in taskList) { task.ContinueWith(t => { if (task.Status == TaskStatus.RanToCompletion) tcs.TrySetResult(t.Result); else if (Interlocked.Decrement(ref remainingTasks) == 0) tcs.SetException(new AggregateException(tasks.SelectMany(t1 => t1.Exception.InnerExceptions))); }); } return tcs.Task; } 

没有结果的任务版本:

 public static Task FirstSuccessfulTask(IEnumerable tasks) { var taskList = tasks.ToList(); var tcs = new TaskCompletionSource(); int remainingTasks = taskList.Count; foreach (var task in taskList) { task.ContinueWith(t => { if (task.Status == TaskStatus.RanToCompletion) tcs.TrySetResult(true); else if (Interlocked.Decrement(ref remainingTasks) == 0) tcs.SetException(new AggregateException( tasks.SelectMany(t1 => t1.Exception.InnerExceptions))); }); } return tcs.Task; } 

一个直接的解决方案是等待任何任务,检查它是否处于RanToCompletion状态,如果没有,再等待除已经完成的任务之外的任何任务。

  async Task WaitForFirstCompleted( IEnumerable> tasks ) { var taskList = new List>( tasks ); Task firstCompleted; while ( taskList.Count > 0 ) { firstCompleted = await Task.WhenAny( taskList ); if ( firstCompleted.Status == TaskStatus.RanToCompletion ) { return firstCompleted.Result; } taskList.Remove( firstCompleted ); } throw new InvalidOperationException( "No task completed successful" ); } 

@Servy代码的修改版本,因为它包含一些编译错误和一些陷阱。 我的变体是:

 public static class AsyncExtensions { public static Task GetFirstSuccessfulTask(this IReadOnlyCollection> tasks) { var tcs = new TaskCompletionSource(); int remainingTasks = tasks.Count; foreach (var task in tasks) { task.ContinueWith(t => { if (task.Status == TaskStatus.RanToCompletion) tcs.TrySetResult(t.Result); else if (Interlocked.Decrement(ref remainingTasks) == 0) tcs.SetException(new AggregateException( tasks.SelectMany(t2 => t2.Exception?.InnerExceptions ?? Enumerable.Empty()))); }); } return tcs.Task; } } 

我们没有ToList我们的输入,因为它已经是我们可以使用的集合,它编译(巨大的优势)并且它处理由于某种原因exception没有一个内在exception(它完全可能)的情况。