0

我还在学习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显式等待替换最后一个会产生影响?

4

3 回答 3

2

您的替换版本会阻塞等待任务完成的调用线程。很难在这样的控制台应用程序中看到明显的差异,因为您故意在 Main 中阻塞,但它们绝对不是等效的。

于 2012-09-29T14:44:08.047 回答
2

它们不是等价的。

Task.Result阻塞,直到结果可用。正如我在博客中解释的那样,如果您有一个需要独占访问的上下文(例如,UI 或 ASP.NET 应用程序) ,这可能会导致死锁。async

此外,Task.Result会将任何异常包装在 中AggregateException,因此如果您同步阻塞,则错误处理会更加困难。

于 2012-09-29T14:46:30.483 回答
0

好的,我想我想通了,所以让我总结一下,希望能比迄今为止提供的答案更完整的解释......

简答

用显式等待替换第二个await对控制台应用程序没有明显影响,但会在等待期间阻塞 WPF 或 WinForms 应用程序的 UI 线程。

此外,异常处理略有不同(如 Stephen Cleary 所述)。

长答案

简而言之,这样await做:

  1. 如果等待的任务已经完成,它只是检索其结果并继续。
  2. 如果没有,它会将延续(在 之后的方法的其余部分)发布await当前同步上下文(如果有的话)。从本质上讲,await是试图让我们回到我们开始的地方。
    • 如果没有当前上下文,它只使用原始的TaskScheduler,通常是线程池。

第二个(和第三个等等......)也是如此await

由于控制台应用程序通常没有同步上下文,延续通常由线程池处理,因此如果我们在延续中阻塞也没有问题。

另一方面,WinForms 或 WPF 在其消息循环之上实现了同步上下文。因此,await在 UI 线程上执行(最终)也会在 UI 线程上执行其延续。如果我们碰巧在 continuation 中阻塞,它将阻塞消息循环并使 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;
            }
        );
    }

}

如果我们将第二个替换为awaitFooAsynct2.Result会在按钮单击后继续响应约 3 秒,然后冻结约 2 秒:

  • 第一个之后的继续await将礼貌地等待轮到在 UI 线程上安排,这将在Method1Async()任务完成后发生,即大约 3 秒后,
  • 此时t2.Result将粗暴地阻塞 UI 线程,直到Method2Async()任务完成,大约 2 秒后。

如果我们删除async前面的button1_Click并用它替换awaitFooAsync().Result会死锁:

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

Stephen Toub的文章“Await、SynchronizationContext 和控制台应用程序”对我理解这个主题非常宝贵。

于 2012-09-30T20:28:58.510 回答