我还在学习async
/ await
,所以如果我问一些明显的话,请原谅。请考虑以下示例:
class Program {
static void Main(string[] args) {
var result = FooAsync().Result;
Console.WriteLine(result);
}
static async Task<int> FooAsync() {
var t1 = Method1Async();
var t2 = Method2Async();
var result1 = await t1;
var result2 = await t2;
return result1 + result2;
}
static Task<int> Method1Async() {
return Task.Run(
() => {
Thread.Sleep(1000);
return 11;
}
);
}
static Task<int> Method2Async() {
return Task.Run(
() => {
Thread.Sleep(1000);
return 22;
}
);
}
}
此操作符合预期,并在控制台中打印“33”。
如果我用明确的等待替换第二 await
...
static async Task<int> FooAsync() {
var t1 = Method1Async();
var t2 = Method2Async();
var result1 = await t1;
var result2 = t2.Result;
return result1 + result2;
}
......我似乎也有同样的行为。
这两个例子完全相同吗?
如果在这种情况下它们是等效的,是否有其他情况通过明确的等待替换最后的await
会产生影响?
答案 0 :(得分:2)
您的替换版本会阻止等待任务完成的调用线程。很难在控制台应用程序中看到明显的区别,因为你在Main中有意阻塞,但它们肯定不等同。
答案 1 :(得分:2)
它们并不等同。
Task.Result
阻止,直到结果可用。正如我在我的博客上解释的那样,can cause deadlocks如果你有async
上下文需要独占访问权限(例如,UI或ASP.NET应用程序)。
此外,Task.Result
将在AggregateException
中包装任何异常,因此如果同步阻止,错误处理会更难。
答案 2 :(得分:0)
好吧,我想我已经想到了这一点,所以让我总结一下,希望是比迄今为止提供的答案更完整的解释......
使用显式等待替换第二个await
对控制台应用程序没有明显影响,但会在等待期间阻止WPF或WinForms应用程序的UI线程。
此外,异常处理略有不同(正如Stephen Cleary所述)。
简而言之,await
执行此操作:
await
之后的方法的其余部分)到current同步上下文,如果有的话。从本质上讲,await
正试图让我们回到原点。
第二个(和第三个等等......)await
也是如此。
由于控制台应用程序通常没有同步上下文,因此延迟通常由线程池处理,因此如果我们在延续中阻塞,则没有问题。
另一方面, WinForms或WPF在其消息循环之上实现了同步上下文。因此,在UI线程上执行的await
也将(最终)在UI线程上执行其继续。如果我们碰巧在延续中阻塞,它将阻止消息循环并使UI无响应,直到我们解除阻塞。 OTOH,如果我们只是await
,它将整齐地发布最终在UI线程上执行的延续,而不会阻止UI线程。
在以下WinForms表单中,包含一个按钮和一个标签,使用await
始终保持UI响应(请注意点击处理程序前面的async
):
public partial class Form1 : Form {
public Form1() {
InitializeComponent();
}
private async void button1_Click(object sender, EventArgs e) {
var result = await FooAsync();
label1.Text = result.ToString();
}
static async Task<int> FooAsync() {
var t1 = Method1Async();
var t2 = Method2Async();
var result1 = await t1;
var result2 = await t2;
return result1 + result2;
}
static Task<int> Method1Async() {
return Task.Run(
() => {
Thread.Sleep(3000);
return 11;
}
);
}
static Task<int> Method2Async() {
return Task.Run(
() => {
Thread.Sleep(5000);
return 22;
}
);
}
}
如果我们将await
中的第二个FooAsync
替换为t2.Result
,它会在按钮点击后继续响应约3秒,然后冻结约2秒:< / p>
await
之后的延续将礼貌地等待在UI线程上安排,这将在Method1Async()
任务完成后,即大约3秒后发生,t2.Result
将严重阻止UI线程,直到Method2Async()
任务完成,大约2秒后。如果我们移除了async
前面的button1_Click
并将其await
替换为FooAsync().Result
,那么就会陷入僵局:
FooAsync()
任务完成,FooAsync().Result
阻止。Stephen Toub的文章"Await, SynchronizationContext, and Console Apps"对我理解这个主题非常宝贵。