最后等待可以用显式等待替换吗?

时间:2012-09-29 14:23:31

标签: c# .net asynchronous .net-4.5 async-await

我还在学习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会产生影响?

3 个答案:

答案 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执行此操作:

  1. 如果等待的任务已经完成,它只是检索其结果并继续。
  2. 如果没有,则posts继续(await之后的方法的其余部分)到current同步上下文,如果有的话。从本质上讲,await正试图让我们回到原点。
    • 如果没有当前上下文,它只使用原始的TaskScheduler,通常是线程池。
  3. 第二个(和第三个等等......)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,那么就会陷入僵局:

    • UI线程将等待FooAsync()任务完成,
    • 会等待继续完成,
    • 会等待UI线程变为可用,
    • 它不是,因为它被FooAsync().Result阻止。

    Stephen Toub的文章"Await, SynchronizationContext, and Console Apps"对我理解这个主题非常宝贵。