多个await和Task.WaitAll - 是否等价?

79

就性能而言,这两种方法会同时运行GetAllWidgets()GetAllFoos()吗?

选择一种方法比另一种更有理由吗?编译器在幕后进行了很多操作,所以我不是很清楚。

============= 方法A:使用多个awaits ======================

public async Task<IHttpActionResult> MethodA()
{
    var customer = new Customer();

    customer.Widgets = await _widgetService.GetAllWidgets();
    customer.Foos = await _fooService.GetAllFoos();

    return Ok(customer);
}

=============== 方法B:使用Task.WaitAll =====================

public async Task<IHttpActionResult> MethodB()
{
    var customer = new Customer();

    var getAllWidgetsTask = _widgetService.GetAllWidgets();
    var getAllFoosTask = _fooService.GetAllFos();

    Task.WaitAll(new List[] {getAllWidgetsTask, getAllFoosTask});

    customer.Widgets = getAllWidgetsTask.Result;
    customer.Foos = getAllFoosTask.Result;

    return Ok(customer);
}
抱歉,我只能使用英文回答您的问题。

3
在您提供的第一个示例中,这两种方法将按顺序调用,在第二个示例中它们将并行运行,因此它们不相同。另外,在第二个方法中,您在执行任务时会阻塞。 - Daniel Kelley
_widgetService.GetAllWidgets() 完成后,MethodA 将执行 _fooService.GetAllFoos()。而 methodB 则会在 _fooService.GetAllFoos() 的未完成任务返回时执行它。 - shay__
@DanielKelley 第二个不保证它们会并行运行。正如其他答案和异步操作的解释经常强调的那样,它们不一定是多线程或并行的。有些人甚至坚称它们根本不是多线程的,但这是一个不必要的限制。根据实现方式,它们可能会并行运行,但最强的说法是它们不能保证按顺序运行。 - C Perkins
5个回答

140

第一种选项不会并发执行两个操作。它将执行第一个操作并等待其完成,然后才执行第二个操作。

第二种选项将同时执行这两个操作,但会同步等待它们的完成(即阻塞一个线程)。

你不应该同时使用这两个选项,因为第一个选项完成得比第二个慢,而第二个选项没有必要阻塞一个线程。

你应该使用Task.WhenAll异步等待这两个操作的完成:

public async Task<IHttpActionResult> MethodB()
{
    var customer = new Customer();

    var getAllWidgetsTask = _widgetService.GetAllWidgets();
    var getAllFoosTask = _fooService.GetAllFos();

    await Task.WhenAll(getAllWidgetsTask, getAllFoosTask);

    customer.Widgets = await getAllWidgetsTask;
    customer.Foos = await getAllFoosTask;

    return Ok(customer);
}
请注意,Task.WhenAll 完成后,两个任务已经完成,因此等待它们的完成将立即完成。

2
谢谢。这正是我需要的。".Result" 一直困扰着我,你的答案避免了这个问题。 - vidalsasoon
14
你也可以完全跳过 await Task.WhenAll(getAllWidgetsTask, getAllFoosTask);,直接等待任务(只需在等待第一个任务之前启动第二个任务)。 - kwesolowski
3
@kwesoly说得没错,但是只“挂起”一次会更加高效。 - i3arnon
1
@i3arnon - 听起来是个很好的理由,没有必要为了再次等待而切换上下文。 - kwesolowski
5
这两者之间只有一个区别。Result会将任何异常都包装在AggregateException中,这意味着你需要在所有的错误处理代码中解开它。而await则为你自动解开。除此之外,它们是完全相同的。 - Servy
显示剩余11条评论

25

简短回答:不行。

Task.WaitAll 是阻塞的,而 await 则是遇到任务后立即返回该任务,并注册函数的剩余部分和后续操作。

你正在寻找的“批量”等待方法是 Task.WhenAll,它实际上创建一个新的 Task,当所有传递给函数的任务都完成时,它就会结束。

像这样:await Task.WhenAll({getAllWidgetsTask, getAllFoosTask});

这是关于阻塞问题的解决方法。

此外,你的第一个函数并没有并行执行两个函数。要使用 await 来实现并行执行,你需要编写类似下面这样的代码:

var widgetsTask = _widgetService.GetAllWidgets();
var foosTask = _fooService.GetAllWidgets();
customer.Widgets = await widgetsTask;
customer.Foos = await foosTask;

这将使第一个示例的行为与Task.WhenAll方法非常相似。


1
作为对@i3arnon所说的内容的补充。你会发现,当你使用await时,你被迫声明封装方法为async,但是使用waitAll则不需要。这应该告诉你,它比主要答案所说的更复杂。以下是详细说明: WaitAll将阻塞,直到给定的任务完成,而在这些任务运行时,它不会将控制权返回给调用者。同样如上所述,这些任务是异步运行的,而不是针对调用者。 Await不会阻塞调用者线程,但它会暂停下面的代码执行,但当任务正在运行时,控制权会返回给调用者。由于控制权返回给调用者(调用的方法正在异步运行),因此必须将该方法标记为异步。
希望区别很清楚。干杯

0
只有你的第二个选项会并行运行它们。第一个选项会按顺序等待每个调用完成。

0

一旦调用异步方法,它就会开始执行。无法确定它是否在当前线程上执行(因此同步运行)还是异步运行。

因此,在您的第一个示例中,第一个方法将开始执行工作,但是您使用await人为地停止了代码流程。因此,在第一个方法执行完成之前,不会调用第二个方法。

第二个示例在不使用await停止流程的情况下同时调用两种方法。因此,如果这些方法是异步的,则它们可能并行运行。


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