是否应该等待嵌套的可等待操作?

Should nested awaitable operations be awaited?

我一直在关注 and I understand the reasons behind the popular (albeit as-yet-unaccepted) answer by Peter Duniho。具体来说,我知道 not 等待后续的 long-运行 操作将阻塞 UI 线程:

The second example does not yield during the asynchronous operation. Instead, by getting the value of the content.Result property, you force the current thread to wait until the asynchronous operation has completed.

我什至已经确认了这一点,为了我自己的利益,就像这样:

private async void button1_Click(object sender, EventArgs e)
{
    var value1 = await Task.Run(async () =>
        {
            await Task.Delay(5000);
            return "Hello";
        });

    //NOTE: this one is not awaited...
    var value2 = Task.Run(async () =>
        {
            await Task.Delay(5000);
            return value1.Substring(0, 3);
        });

    System.Diagnostics.Debug.Print(value2.Result); //thus, UI freezes here after 5000 ms.
}

但现在我想知道:是否需要 await 所有 "awaitable" 操作都嵌套在最外层的可等待操作中?例如,我可以这样做:

private async void button1_Click(object sender, EventArgs e)
{
    var value0 = await Task.Run(() =>
        {
            var value1 = new Func<Task<string>>(async () =>
            {
                await Task.Delay(5000);
                return "hello";
            }).Invoke();

            var value2 = new Func<string, Task<string>>(async (string x) =>
                {
                    await Task.Delay(5000);
                    return x.Substring(0, 3);
                }).Invoke(value1.Result);

            return value2;
        });

    System.Diagnostics.Debug.Print(value0); 
}

或者我可以这样做:

private async void button1_Click(object sender, EventArgs e)
{
    //This time the lambda is async...
    var value0 = await Task.Run(async () =>
        {
            //we're awaiting here now...
            var value1 = await new Func<Task<string>>(async () =>
            {
                await Task.Delay(5000);
                return "hello";
            }).Invoke();

            //and we're awaiting here now, too...
            var value2 = await new Func<string, Task<string>>(async (string x) =>
                {
                    await Task.Delay(5000);
                    return x.Substring(0, 3);
                }).Invoke(value1);

            return value2;
        });

    System.Diagnostics.Debug.Print(value0); 
}

而且他们都没有冻结 UI。哪个更好?

最好是最后一个(虽然比较乱)

在 TAP(基于任务的异步模式)中,任务(和其他可等待对象)表示异步操作。您基本上有 3 个选项来处理这些任务:

  • 同步等待 (DoAsync().Result, DoAsync().Wait()) - 阻塞调用线程直到任务完成。使您的应用程序更加浪费,可扩展性更差,响应更慢并且容易出现死锁。
  • 异步等待 (await DoAsync()) - 不阻塞调用线程。它基本上将 await 之后的工作注册为等待任务完成后要执行的延续。
  • 根本不等待 (DoAsync()) - 不阻塞调用线程,也不等待操作完成。您不知道在处理 DoAsync 时抛出的任何异常

Specifically, I am aware that not awaiting a subsequent long-running operation will block the UI thread

所以,不完全是。如果你根本不等待,什么都不会阻塞,但你无法知道操作何时或是否成功完成。但是,如果您同步等待,您将阻塞调用线程,并且如果您阻塞 UI 线程,则可能会出现死锁。

结论:您应该 await 尽可能长的可等待对象(例如,它不在 Main 中)。这包括 "nested async-await operations".

关于您的具体示例:Task.Run 用于将 CPU 绑定的工作卸载到 ThreadPool 线程,这似乎不是您要模仿的。如果我们使用 Task.Delay 来表示一个真正的异步操作(通常是 I/O-bound),我们可以有 "nested async-await" 而没有 Task.Run

private async void button1_Click(object sender, EventArgs e)
{
    var response = await SendAsync();
    Debug.WriteLine(response); 
}

async Task<Response> SendAsync()
{
    await SendRequestAsync(new Request());
    var response = await RecieveResponseAsync();
    return response;
}

async Task SendRequestAsync(Request request)
{
    await Task.Delay(1000); // actual I/O operation
}

async Task<Response> RecieveResponseAsync()
{
    await Task.Delay(1000); // actual I/O operation
    return null;
}

您可以使用匿名委托而不是方法,但是当您需要定义类型并自己调用它们时会很不方便。

如果您确实需要将该操作卸载到 ThreadPool 线程,只需添加 Task.Run:

private async void button1_Click(object sender, EventArgs e)
{
    var response = await Task.Run(() => SendAsync());
    Debug.WriteLine(response); 
}