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

时间:2015-01-29 17:56:00

标签: c# .net asynchronous task-parallel-library async-await

我一直关注this question,我理解Peter Duniho之后流行(尽管尚未接受)答案背后的原因。具体来说,我知道等待后续长时间运行的 not 将阻止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.
}

但是现在我想知道:你需要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); 
}

或者我可以这样做:

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。哪个更好?

1 个答案:

答案 0 :(得分:13)

最后一个是优选的(尽管非常混乱)

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

  • 等待同步(DoAsync().ResultDoAsync().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;
}

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

如果您确实需要将该操作卸载到ThreadPool个帖子,只需添加Task.Run

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