Task.WhenAll 但一个一个处理结果

3

假设我有一个任务列表,并且我想要并行运行它们。但我不需要等待全部任务完成才能继续,只要其中一个完成就可以继续进行。以下代码需要等待所有任务完成后才能继续。有没有办法在等待其他任务完成的同时,单独继续已完成的任务?

List<string>[] x = await Task.WhenAll(new Task<List<string>>[] { task1, task2 })
// When task1 finishes, I want to process the result immediately
// instead of waiting on task2.

1
ContinueWith? - ProgrammingLlama
5
可以使用WhenAny吗?https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.whenany?view=netcore-3.1 请参阅https://blog.stephencleary.com/2015/01/a-tour-of-task-part-7-continuations.html - Fildor
5个回答

6
你可能正在寻找Task.WhenAny
我用它来启动一堆任务,然后在它们准备好时逐个处理,但如果你不关心处理剩下的任务,也可以等待其中一个完成后继续进行而不使用循环。
while (tasks.Count() > 0)
{
    var task = await Task.WhenAny(tasks);
    tasks.Remove(task);
    var taskresult = await task;
    // process result
}

5
如果你使用的是C# 8和.NET Core,你可以利用 IAsyncEnumerable 来隐藏这种复杂性,使消费方更易于理解。
就像这样:
static async Task Main(string[] args)
{
    await foreach (var data in GetData())
    {
        Console.WriteLine(data);
    }

    Console.ReadLine();
}

static async IAsyncEnumerable<string> GetData()
{
    List<Task<string>> tasks = new List<Task<string>> {GetData1(), GetData3(), GetData2()};

    while (tasks.Any())
    {
        var finishedTask = await Task.WhenAny(tasks);
        tasks.Remove(finishedTask);
        yield return await finishedTask;
    }
}

static async Task<string> GetData1()
{
    await Task.Delay(5000);
    return "Data1";
}

static async Task<string> GetData2()
{
    await Task.Delay(3000);
    return "Data2";
}

static async Task<string> GetData3()
{
    await Task.Delay(2000);
    return "Data3";
}

2
你可以使用Task.WhenAny代替。
Stephen Cleary's Blog“偷”来的例子:
var client = new HttpClient();
string results = await await Task.WhenAny(
    client.GetStringAsync("http://example.com"),
    client.GetStringAsync("http://microsoft.com"));
// results contains the HTML for whichever website responded first.

回复评论
你绝对可以跟踪其他任务:
// supposing you have a list of Tasks in `myTasks`:
while (myTasks.Count > 0)
{
    var finishedTask = await Task.WhenAny(myTasks);
    myTasks.Remove(finishedTask);
    handleFinishedTask(finishedTask); // assuming this is a method that
                                      // does the work on finished tasks.
}

唯一需要注意的是:
返回的任务总是以“RanToCompletion”状态结束,并将其结果设置为第一个完成的任务。即使第一个完成的任务处于已取消或故障状态,这也是正确的。 Task.WhenAny文档中的备注(我强调)。

听起来不错,但我仍然想要那些没有完成的人的回应。有没有办法做到这一点? - idude
当然。如果您只使用一个await,则会得到已完成的任务,并可以将其从列表中删除。就像Richard的回答一样。在Stephen Cleary的博客中还有许多其他示例。阅读起来真的很值得,作者的所有内容都是如此。 - Fildor

1

如果您希望按照任务完成的顺序处理结果,可以使用Stephen Cleary的Nito.AsyncEx库中的OrderByCompletion扩展方法,其签名如下:

// Creates a new collection of tasks that complete in order.
public static List<Task<T>> OrderByCompletion<T>(this IEnumerable<Task<T>> @this);

使用示例:
Task<string>[] tasks = new[] { task1, task2, task3, task4 };
foreach (var task in tasks.OrderByCompletion())
{
    string result = await task;
    // Do something with result
}

0

根据Peter Csala的回答,这里提供一个IAsyncEnumerable的扩展方法:

    public static async IAsyncEnumerable<T> OrderedByCompletion<T>(this IEnumerable<Task<T>> tasks)
    {
        List<Task<T>> taskList = new List<Task<T>>(tasks);

        while (taskList.Count > 0)
        {
            var finishedTask = await Task.WhenAny(taskList);
            taskList.Remove(finishedTask);
            yield return await finishedTask;
        }
    }

3
请注意,await Task.WhenAny 循环中的反模式是一种效率低下的反模式,其复杂度为 O(n²)。它可以工作,但不适合扩展。 - Theodor Zoulias
@TheodorZoulias 是的。你可以对此进行很多优化,但只要这不是性能关键点,我建议你坚持使用它。有许多更快的实现方式,从使用字典开始,到使用不运行循环的不同实现结束。 - Felix K.
Felix,即使您将List<T>切换为Dictionary<K,V>,此模式的复杂度仍然是O(n²)。 - Theodor Zoulias
Felix,你不需要做任何特殊的事情。它已经高效地实现了至少三次。只需选择Jon SkeetStephen ToubStephen Cleary的其中一个实现即可完成。 - Theodor Zoulias
不,这是 IAsyncEnumerable<T>,它与 IEnumerable<T> 不同。最终结果几乎相同,但编写方式和使用方式不同。我知道 Skeet 的实现并曾经使用过它。 - Felix K.
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接