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

ror*_*.ap 8 .net c# asynchronous task-parallel-library async-await

我一直在关注这个问题,我理解Peter Duniho的流行(尽管尚未接受)答案背后的原因.具体来说,我知道等待后续的长时间运行将阻止UI线程:

第二个示例在异步操作期间不会产生.相反,通过获取content.Result属性的值,可以强制当前线程等待异步操作完成.

为了我自己的利益,我甚至证实了这一点,如下:

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.
}
Run Code Online (Sandbox Code Playgroud)

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

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); 
}
Run Code Online (Sandbox Code Playgroud)

或者我可以这样做:

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); 
}
Run Code Online (Sandbox Code Playgroud)

他们都没有冻结UI.哪个更好?

i3a*_*non 13

最后一个是可取的(虽然非常凌乱)

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

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

具体来说,我知道不等待随后的长时间运行将阻止UI线程

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

结论:await只要可能(Main例如,不是这样),你就应该等待.这包括"嵌套async-await操作".

关于您的具体示例:Task.Run用于将CPU绑定的工作卸载到ThreadPool线程,这似乎不是您试图模仿的.如果我们Task.Delay用来表示真正的异步操作(通常是I/O绑定),我们就可以"嵌套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;
}
Run Code Online (Sandbox Code Playgroud)

您可以使用匿名委托而不是方法,但是当您需要定义类型并自己调用它们时会感到不舒服.

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

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