何时使用"await"关键字

23

我正在编写一个网页,它调用了一些网络服务。这些调用看起来像这样:

var Data1 = await WebService1.Call();
var Data2 = await WebService2.Call();
var Data3 = await WebService3.Call();

在代码审查期间,有人建议我将其更改为:

var Task1 = WebService1.Call();
var Task2 = WebService2.Call();
var Task3 = WebService3.Call();

var Data1 = await Task1;
var Data2 = await Task2;
var Data3 = await Task3;

为什么?有什么区别吗?


除非是某种复制问题,否则我不认为一个好的编译器不会优化掉多余的赋值。虽然我对C#不够了解,但也许这只是一个语义问题,你的团队更喜欢明确表示调用是一个任务,而任务的结果是数据? - JAB
10
@JAB 那不是真的。这两个代码片段在语义含义上有着显著的不同之处。 - Servy
@JAB 你能想到更好的吗?(不过现在也无法改变了。)很多人花了很多时间思考什么是合适的。 - Servy
await 实际上意味着“将下一个表达式评估为任务(通常是启动它),在该任务中添加一些继续执行当前方法的代码,然后停止当前上下文中的执行。如果你愿意,可以将其视为异步等待,但这是一个非常容易出问题的抽象概念。 - Servy
@Eric 你应该针对新的问题发表一个新的问题,而不是编辑现有的问题。 然而,与您发布的代码相同,只是您的结果存储在数组中,而不是3个变量中。 - Scott Chamberlain
显示剩余8条评论
3个回答

38
Servy的回答是正确的,稍微展开一下。以下两者之间有何不同:
Eat(await cook.MakeSaladAsync());
Eat(await cook.MakeSoupAsync());
Eat(await cook.MakeSandwichAsync());

Task<Salad> t1 = cook.MakeSaladAsync();
Task<Soup> t2 = cook.MakeSoupAsync();
Task<Sandwich> t3 = cook.MakeSandwichAsync();
Eat(await t1);
Eat(await t2);
Eat(await t3);

?

第一个方案:

  • 厨师,请给我做个沙拉。
  • 在等沙拉的时候,你有一些空闲时间可以为猫刷毛。当你完成后,哦,看,沙拉已经做好了。如果在你刷猫的时候厨师已经做好了沙拉,他们没有开始做汤因为你还没要求
  • 吃沙拉。当你吃东西时,厨师现在是空闲的
  • 厨师,请给我做点汤。
  • 在等汤的时候,你有一些空闲时间可以清洁鱼缸。当你完成后,哦,看,汤已经做好了。如果在你清洗鱼缸的时候厨师已经做好了汤,他们没有开始做三明治,因为你还没要求
  • 吃汤。当你吃东西时,厨师现在是空闲的
  • 厨师,请给我做个三明治。
  • 同样,找点别的事情做,直到三明治准备好。
  • 吃三明治。

你的第二个方案等价于:

  • 厨师,请给我做个沙拉。
  • 厨师,请给我做点汤。
  • 厨师,请给我做个三明治。
  • 沙拉做好了吗?如果没有,在等沙拉的时候,你有一些空闲时间可以为猫刷毛。如果在你刷猫的时候厨师已经做好了沙拉,他们会开始做汤
  • 吃沙拉。 当你吃东西时,厨师可以继续制作汤和三明治。
  • 汤做好了吗?……

看到区别了吗?在原始方案中,直到你吃完第一道菜,你才告诉厨师开始下一道菜。在第二个方案中,你预先请求了所有三道菜,并按顺序将它们服务出来。第二个方案更好地利用了厨师的时间,因为厨师可以"超前"你。


1
我认为需要注意的是,厨师可以同时制作多道菜肴,这似乎没有明确说明。因此,在第二种情况下,当您要求所有3道菜时,厨师可能能够在炉子上加热汤时制作沙拉。这就是为什么在第一种情况下,让厨师一次只做一件事是没有意义的,因为他们可以同时制作多道菜肴。 - Letseatlunch
@Letseatlunch:这是一个好观点,但对于我的论点并非必要;这里相关的观点是,当您首先要求所有三项时,服务提供商可以一次性完成它们所有,或者一个一个地完成,但至少他们知道要完成所有三个。如果您在第一件事完成之前不要求第二件事,则可能未充分利用资源。 - Eric Lippert

33

在第一个代码片段中,直到第一次服务调用完成(同样地,在第二个完成之前不会开始第三个),你甚至没有启动第二个服务调用。简言之,它们是按顺序执行的。

在第二个片段中,您启动了所有三个服务调用,但然后在代码中不继续执行,直到所有三个调用完成。简言之,它们都是并行执行的。

如果第二/第三个调用在获得前一个操作的结果之前无法“开始”,则您需要像第一个片段那样做才能使其正常工作。如果服务调用根本不相互依赖,则出于性能原因,您希望它们并行执行。

如果出于某种原因,您真的不喜欢有额外的本地变量,则可以使用其他语法的替代方法以并行方式执行任务。其中一个像您第二个选项一样的替代方案是:

var Data = await Task.WhenAll(WebService1.Call(), 
    WebService2.Call(), 
    WebService3.Call());

1
带有 await 的线程将停止,但两个任务将继续进行。 - Sergey Kalinichenko
@StephenCleary 是的,在你评论时我已经在编辑那部分了。看起来我在5分钟之前就已经把它放进去了。 ;) - Servy
@StephenCleary,那么Task.WhenAll(...)和3个awaits不是一样的吗? - Eric B
@EricB 根据我的回答,使用 WhenAll 在功能上与第二个片段等效。 它不等同于第一个片段。 - Servy
2
@EricB:WhenAll会更有效率,个人认为它更好地捕捉了所需的语义(“等待所有这些完成”而不是“等待第一个,然后第二个,然后第三个”)。 - Stephen Cleary
显示剩余2条评论

1

Servy posted了一个非常好的答案,但是这里使用Task的可视化描述可以帮助展示问题所在。这段代码与你的功能不同(它并未执行所有同步上下文的操作,例如将控制权返还给消息泵),但它很好地说明了问题。

你的代码正在做类似于以下操作:

var fooTask = Task.Factory.StartNew(Foo);
fooTask.Wait();
var fooResult = fooTask.Result;

var barTask = Task.Factory.StartNew(Bar);
barTask.Wait();
var barResult = barTask.Result;

var bazTask = Task.Factory.StartNew(Baz);
bazTask.Wait();
var bazResult = bazTask.Result;

经过修正的代码大致做了以下几件事情

var fooTask = Task.Factory.StartNew(Foo);
var barTask = Task.Factory.StartNew(Bar);
var bazTask = Task.Factory.StartNew(Baz);

fooTask.Wait();
var fooResult = fooTask.Result;
barTask.Wait();
var barResult = barTask.Result;
bazTask.Wait();
var bazResult = bazTask.Result;

你可以看到,所有三个任务都在等待第一个结果返回时运行,在第一个示例中,第二个任务直到第一个任务完成后才开始运行。

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