【问题标题】:Should nested awaitable operations be awaited?是否应该等待嵌套的可等待操作?
【发布时间】:2015-01-29 17:55:59
【问题描述】:

我一直在关注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。哪一个更可取?

【问题讨论】:

  • 如果它们都打印相同的东西,我会感到非常惊讶,我相信打印的是一个任务对象而不是它的结果。
  • @BenVoigt -- 不,在这两种情况下,任务都被解包,因为它正在等待。
  • 但其中一个正在包装第二个任务。
  • @BenVoigt -- 我明白你的意思。这很奇怪。不过,value0 在这两种情况下都是 string,但它确实打印了相同的内容。\
  • 啊,Run&lt;TResult&gt;(Func&lt;TResult&gt;)Run&lt;TResult&gt;(Func&lt;Task&lt;TResult&gt;&gt;) 有重载,额外层的展开是在那里进行的。

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


【解决方案1】:

最后一个更可取(虽然很乱)

在 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 绑定的),我们可以在没有 Task.Run 的情况下使用“嵌套 async-await”:

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); 
}

【讨论】:

  • 实际上,我正在再次阅读您的答案,我想知道您是否可以澄清一下:您是说我不应该使用嵌套等待(即我的第三个代码块在做什么——“或者我可以这样做”之后的那个)?
  • @roryap 我是说如果操作是async 然后await 他们而不是阻塞,无论他们在哪里。关于是否使用Task.Run 零、一次或两次,这取决于您实际上 想要做什么(我假设Task.Delay 只是一个替代品)。
  • @roryap 如果您想要卸载一个长时间的 CPU 密集型操作,请使用 Task.Run,否则只需调用您的 async 操作和 await 它们。
  • 我问的唯一原因是因为您在答案中包含的代码没有任何嵌套等待(与 Task.Delay 一起使用的代码除外),这让我相信您正在尝试说根本不应该使用这种方法。您的代码似乎是如何重构我的 third 代码块以获得相同结果的示例,但我故意将其作为嵌套等待的一次性示例。我只是想把这一切都包起来。
  • @roryap 嵌套的async-awaits 没有任何问题。我会更新答案以反映这一点。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-03-15
  • 2013-08-10
  • 2017-02-11
  • 1970-01-01
  • 2015-02-02
  • 2020-01-28
  • 1970-01-01
相关资源
最近更新 更多