如何在控制台应用程序中从 Task.WaitAll() 获取返回值?

74

我正在使用控制台应用程序作为概念验证,并且需要获取异步返回值。

我发现我需要在主方法中使用 Task.WaitAll() 来避免需要一个异步的 "main()" 方法,因为这是不合法的。

现在我卡在了试图找到允许我在 Main() 中使用泛型或仅返回可转换对象的重载上。

3个回答

119

使用 Task.WaitAll 方法不会返回任何值。它只用于等待多个任务完成,然后从这些任务本身获取返回值。

var task1 = GetAsync(1);
var task2 = GetAsync(2);
Task.WaitAll(task1, task2);
var result1 = task1.Result;
var result2 = task2.Result;
如果你只有一个单一的Task,只需使用Result属性。 它将返回您的值并阻塞调用线程(如果任务尚未完成):
var task = GetAsync(3);
var result = task.Result;

通常情况下,同步等待(阻塞)异步任务并不是一个好主意(“sync over async”),但对于 POC 来说可能还可以接受。



2
不确定Task.WaitAll()的实现是否有所改变,但这个答案对我没有用。Task.WaitAll()只会阻塞线程,执行永远不会到达下一行。如其他答案所建议,使用await Task.WhenAll()似乎是未来推荐的方式。 - Adrian Hristov
3
@AdrianHristov 没有改变,你可能在代码中遇到了SynchronizationContext死锁。正如这个答案所说,阻塞异步任务不是一个好主意,但如果你决定这样做,这就是获取任务结果的方法。 - i3arnon
我不明白这两者之间有什么区别:for(var i = 0; i < 100; i++) { taskList.Add(client.PostAsync("http://localhost:3000...)); } await Task.WhenAll(taskList); foreach(var task in taskList) { System.Diagnostics.Debug.WriteLine(task.Result); }在获取task.Result时出错,因为Task上没有Result属性或方法? - Kevin Burton
@KevinBurton 如果返回类型是Task,你可以使用Wait() [或其中一个重载]。如果返回类型是Task<T>,其中T是返回类型,你可以使用Result。 - sscheider

60

为了最佳实践,请使用新的异步操作方式。替换为:

  • Task.WaitAll 使用 await Task.WhenAll
  • Task.WaitAny 使用 await Task.WhenAny

上述代码可以写成:

var task1 = GetAsync(1);
var task2 = GetAsync(2);
var results = await Task.WhenAll(task1, task2);
var result1 = results[0];
var result2 = results[1];

6
这将强制包含此代码的方法带有async关键字和返回类型为Task。Task.WaitAll将允许您返回void类型。我并不是在说这样做是否更好。 - chris31389
3
避免返回 Task void。 - alltej
4
Task.WaitAll 需要所有任务拥有相同的返回类型 - tchelidze
2
作为对这个讨论的更新,请参考:https://dev59.com/32Qm5IYBdhLWcg3w-S6Y - James Reategui
1
你不应该说“代替”,因为有时我们想要阻塞异步而不是非阻塞。await只是使你的线程变成了非阻塞的。 - RandomEli
为了以后的帮助;如果所有任务都返回一个布尔值,那么可以使用以下代码返回所有结果:var totalResult = results.All(b => b)。(我写这个是因为任务有点神奇,我想表明结果已经失去了那种神秘的氛围) - LosManos

1
有不同的方法可供选择,取决于上下文和您的需求,但我尝试了i3arnon和alltej的答案,在调试过程中,我发现它们都有相同的问题......我在自己创建的控制台应用程序中运行了两个代码示例。我在执行带有Task.WaitAllTask.WhenAll的行时设置了断点。当我检查任务时,我发现它们已经运行-任务属性Status为= ran to completion。事实上,我能够完全删除Task.行,并仍然赋值变量值,所以这是无用的。
我发现真正使这种操作按预期运行并获得所需的返回值是一项非常困难的任务。以下是我想出的解决方案:
Task[] taskArray = {
    Task.Factory.StartNew(() => ExecuteConcurrentTasks(1).Result),
    Task.Factory.StartNew(() => ExecuteConcurrentTasks(2).Result)
};
Task.WaitAll(taskArray);
var s = ((Task<string>)taskArray[0]).Result;

这是一个简单的示例,其中创建了一个任务数组,然后使用Task.WaitAll同时执行两个任务,然后在最后一行显示如何访问第一个任务的返回值。
这是带有更多上下文和更多数据的代码:
private static async void MyAsyncMethod()
{
    Task[] taskArray = {
        Task.Factory.StartNew(() => ExecuteConcurrentTasks(1).Result),
        Task.Factory.StartNew(() => ExecuteConcurrentTasks(2).Result),
        Task.Factory.StartNew(() => ExecuteConcurrentTasks(3).Result),
        Task.Factory.StartNew(() => ExecuteConcurrentTasks(4).Result)
    };
    Task.WaitAll(taskArray);
    string s1 = ((Task<string>)taskArray[0]).Result;
    string s2 = ((Task<string>)taskArray[1]).Result;
    string s3 = ((Task<string>)taskArray[2]).Result;
    string s4 = ((Task<string>)taskArray[3]).Result;
}



private static async Task<string> ExecuteConcurrentTasks(int passedInt)
{
    string s = "Result: " + passedInt.ToString();

    if (passedInt == 4)
    {
        Console.WriteLine(s);
    }
    else if (passedInt == 3)
    {
        Console.WriteLine(s);
    }
    else if (passedInt == 2)
    {
        Console.WriteLine(s);
    }
    else if (passedInt == 1)
    {
        Console.WriteLine(s);
    }

    return s;
}

这是输出结果:

output


我从其他开发人员那里获得了更多关于这些方法的见解。现在我的理解是,.WaitAll会阻塞程序中的其他部分,如果你运行的不仅仅是一个非常简单的程序,这将是一个问题。此外,.WhenAll并不意味着任务直到该行才开始异步执行,它只是意味着该块中的代码除非所有指定的任务都完成运行,否则不会继续执行。

1
我点赞了这个帖子,因为它展示了如何将任务转换为数组以获取结果值。虽然数组本身就是该类型的,但我不明白为什么需要这样做,但确实需要。 - John Lord
1
我的投票反对的原因是 Task.Factory.StartNew(() => ExecuteConcurrentTasks(1).Result) 没有任何理由地阻塞了一个 ThreadPool 线程,并且使用了 原始的 Task.Factory.StartNew 方法而没有明确指定 TaskScheduler。正确的方法是 Task.Run(() => ExecuteConcurrentTasks(1)),或者直接使用 ExecuteConcurrentTasks(1) - Theodor Zoulias

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