如何从WhenAll(任务数组)中获取结果?

3
我有以下代码。
var tasks = new[]
    {
        Task.Factory.StartNew(() => GetSomething1()),
        Task.Factory.StartNew(() => GetSomething2()),
        Task.Factory.StartNew(() => GetSomething3())
    };

var things = Task.WhenAll(tasks);

我该如何分别获取三个任务的结果并打印输出?

1
请点击这里查看 https://dev59.com/4G025IYBdhLWcg3wQDdb - Shahid Manzoor Bhat
如果所有的Get..方法都是异步的(返回自己的Task),你就不需要.StartNew。如果它们都是同步的,并且它们的数量可能是可变的(基于其他集合),考虑使用Enumerable.AsParallel(PLINQ)或Parallel.For[Each]来并行化工作,而不是将它们包装为任务。 - Jeroen Mostert
@DmitryBychenko 这是正确的方向,但是如果在每个任务之后直接使用Result,那么调用WhenAll是多余的。 - Neo
4个回答

0
一种方法是让每个 Task 负责存储自己的结果,然后你只需要 await 任务集合。请注意,你必须使用 await 来执行传递给 WhenAll() 的任务。
var results = new int[3];

var tasks = new[] {
    Task.Factory.StartNew(() => results[0] = GetSomething1()), 
    Task.Factory.StartNew(() => results[1] = GetSomething2()), 
    Task.Factory.StartNew(() => results[2] = GetSomething3())
};

await Task.WhenAll(tasks);

Console.WriteLine(results[0]);
Console.WriteLine(results[1]);
Console.WriteLine(results[2]);

工作演示:https://dotnetfiddle.net/HS32QA

请注意,在使用List<T>而不是数组时,可能需要小心,然后调用list.Add(result),因为无法保证任务执行的顺序或完成时间。


List.Add的真正问题不在于任何顺序,而是它不是线程安全的这个简单事实。你需要使用显式锁或者像ConcurrentBag这样的东西来解决这个问题。不过,到了那个时候,像这样的方法可能会变得不必要复杂,并且只有在每个任务的结果数量是可变的情况下才值得考虑。否则,没有真正的理由不利用任务对结果的内置支持(就像其他答案中使用的方式)。 - Jeroen Mostert

0
你应该在使用When时,采用async..await模式。例如:
  private async Task MyExecution() {
    var tasks = new[] {
      //TODO: Task.Run is a better option than Task.Factory.StartNew
      Task.Factory.StartNew(() => GetSomething1()),
      Task.Factory.StartNew(() => GetSomething2()),
      Task.Factory.StartNew(() => GetSomething3())
    };

    // Await until all tasks are completed 
    await Task.WhenAll(tasks);

    // Since all tasks are completed, we can (safely) query for their `Result`s:
    var things = tasks
      .Select(task => task.Result) // or task => await task
      .ToArray();

    // Let's print the things
    for (int i = 0; i < things.Length; ++i) 
      Console.WriteLine($"Task #{i + 1} returned {things[i]}");

    ... 
  }

1
Task.WhenAll<TResult> 返回一个 Task<TResult[]>。等待此任务将返回一个包含结果的数组。不需要使用 .Select.ToArray 的东西。 - Theodor Zoulias

0
为了分别获取结果,你有多种方法可以做到这一点。我会像这样做:
var task1  = GetSomething1();
var task2  = GetSomething2();
var task3  = GetSomething3();

// your method will continue when everything's completed, but you won't tie up a thread to just hang around until that time.
await Task.WhenAll(task1, task2, task3);

var result1 = await task1;
var result2 = await task2;
var result3 = await task3;

1
这并没有回答问题,如果GetSomethingX方法不返回Task(我怀疑它们不会返回),那么它将无法编译通过。如果它们确实返回Task,那么对WhenAll的调用是多余的,特别是因为不清楚GetSomethingX是返回Task<T>还是Task<Task<T>> - Neo

-1
你有没有考虑过实际使用异步函数?这样你就可以得到一个带有结果的任务数组,并且避免了Task.Factory.StartNew带来的高度不可预测的行为。
private async Task MyExecution() 
{
    var tasks = new[] {
        GetSomething1(),
        GetSomething2(),
        GetSomething3()
    };

    // Await until all tasks are completed 
    await Task.WhenAll(tasks);

    foreach(var t in tasks){
        //t.Result is available
    }
}

public static async Task<int> GetSomething1() { return 1; }
public static async Task<int> GetSomething2() { return 2; }
public static async Task<int> GetSomething3() { return 3; }

在这里试试:https://dotnetfiddle.net/3ffs9L


2
那些“高度不可预测的行为”会是什么呢?封装同步代码确实存在问题,但这些问题并不特定于StartNew。您提供的示例中的async方法本身并不是异步的,可能会误导人们认为只需添加async Task就足以将同步函数转换为异步函数。实际上,在这个特定的代码片段中,无论您是否调用await Task.WhenAll,所有结果都是可用的,因为代码在创建数组时是同步执行的。 - Jeroen Mostert
@JeroenMostert 不确定您为什么认为原始代码包装同步代码。可以在此处找到有关StartNew的一些问题[https://blog.stephencleary.com/2013/08/startnew-is-dangerous.html],[https://sergeyteplyakov.github.io/Blog/async/2019/05/21/The-Dangers-of-Task.Factory.StartNew.html]和[https://devblogs.microsoft.com/pfxteam/task-run-vs-task-factory-startnew/]。我提供的异步方法只是一个示例,您是正确的:它们同步运行。实际上,编译器会警告这一点。 - IOrlandoni
你说得对,这是基于一个假设,即如果Get...方法已经返回了Task,那么就没有理由首先将它们插入.StartNew()中 - 但是提问者可能不知道这一点。如果你能在回答中实际链接到.StartNew()的问题,那么你的回答会更好 - 我不会把那些问题称为“不可预测的”! - Jeroen Mostert

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