TPL的异步等待-异步任务必须链接调用堆栈吗?

4
我的理解是以下内容是不好的实践,因为它会阻塞线程:
Task t = Task.Run(() => MyMethod());
t.Wait();

虽然以下内容不会阻塞线程:
await MyMethod();

然而,要使用await,必须在方法签名中使用async关键字,并返回一个 Task 而不是void或Task<T>.

然而,在n-Tier应用程序中,如果您有一个巨大的调用堆栈,那么我们是否必须像LinqPad中的以下简单示例中所示,将每个调用方法都设置为异步/任务?

async void Main()
{
    int i = await GetNumberD();
    i.Dump();
}

async Task<int> GetNumberD()
{
    return await GetNumberC();
}
async Task<int> GetNumberC()
{
    return await GetNumberB();
}
async Task<int> GetNumberB()
{
    return await GetNumberA();
}
async Task<int> GetNumberA()
{
    await Task.Delay(TimeSpan.FromSeconds(1));
    return 7;
}

2
我认为你不理解异步方法的工作原理。请阅读这篇介绍性文章:https://msdn.microsoft.com/zh-cn/library/hh191443.aspx#BKMK_WhatHappensUnderstandinganAsyncMethod - JustSomeGuy
1
我认为我对异步方法的工作原理非常清楚。但是我不清楚这如何在调用堆栈中传播以及对每个调用方法中的线程产生的影响。如果您可以回复我在下面Otiel的回复中的评论,我将不胜感激。 - DrGriff
2个回答

2

我们每个调用方法都必须是async/Task吗?

如果在堆栈的末尾调用的方法是异步方法,那么是的,你需要这样做。

请注意,文档建议以“Async”为后缀结束方法名称:

async Task<int> GetNumberAsync()

没错 - 栈底的方法确实是一个异步方法。 - DrGriff
如果堆栈底部的方法不立即返回,则调用线程将返回到线程池,直到它完成。如果调用栈中的每个方法都必须是异步任务,那么会有相当多的线程重新分配。看起来有效率的做法应该是:线程33从Main开始,经过D、C、B方法,然后遇到AWAIT后,返回到线程池。线程66从A返回到Main。如果调用栈中的每个方法都有一个AWAIT,那么涉及的线程数量不就更多了吗? - DrGriff
@DrGriff:您所描述的确实是在这种情况下async/await的工作方式(假设没有像UI线程被捕获这样的特殊上下文)-调用Main的线程是调用Task.Delay的同一线程,完成Task.Delay的线程是恢复Main的同一线程。我有一篇async介绍博客文章,您可能会发现有帮助。 - Stephen Cleary
@StephenCleary 这很重要,请允许我再次确认一下。以下陈述是否正确:如果在 OP 的调用堆栈中的叶子方法 async GetNumberA() 是一个异步方法,则调用堆栈中所有方法的链都必须是异步的。因此,如果其中一些 GetNumberB()GetNumberC() 被我的其他方法 DoFoo()DoBar() 所依赖,则这些 DoFoo()DoBar() 也需要是异步的。现在我正在查看我的代码库,并发现超过50%的方法已经被强制成为异步方法。这种情况是否仍然被认为是正常的? - RayLuo
1
@RayLuo:是的,这很正常。解决这个问题的最佳方案是允许异步代码在代码库中自然生长。如果您遵循此解决方案,您将看到异步代码扩展到其入口点,通常是事件处理程序或控制器操作。 - Stephen Cleary

-1

不要这样做,也请不要这样做。

你可以直接等待 Task.Run

await Task.Run(() => MyMethod());

async Task Main()
{
    int i = await Task.Run( () => GetNumberD());
    i.Dump();
}

int GetNumberD()
{
    return GetNumberC();
}
int GetNumberC()
{
    return GetNumberB();
}
int GetNumberB()
{
    return GetNumberA();
}
int GetNumberA()
{
    return 7;
}

我强烈建议您阅读这篇文章:http://blog.stephencleary.com/2013/10/taskrun-etiquette-and-proper-usage.html

编辑 这不是正确的答案。正如评论所述,我误解了CPU绑定任务。


如果GetNumberA()在许多其他地方被调用,那该怎么办?他是不是每次都要写await Task.Run(() => GetNumberA())?将GetNumberA()定义为异步方法会不会更简单和清晰一些呢? - Otiel
这总是取决于具体情况,但在实现中不应使用Task.Run(如果您愿意,可以使用包装器)。阅读stephencleary博客,您就会明白为什么。 - Bart Calixto
1
我的问题的重点是GetNumberA()方法会异步地从互联网下载一些东西,因此必须具有这样的签名:[async Task<int> GetNumberA()]。我只是加入了一个Task.Delay(...)来模拟这个过程。 - DrGriff
@DrGriff 如果是因为可扩展性而异步,那么是的,所有方法都应该是异步的。想一想,如果调用者不是异步的,那么拥有异步方法有什么意义呢?你必须等待响应。 - Bart Calixto
主要目的是为了可扩展性,这样我们就不会阻塞一个线程来处理I/O绑定任务。好的,这正是我所怀疑的。我的担心是这是否是最佳实践 - 我想知道是否可能会捕获调用堆栈中所有方法的状态(局部变量),而不仅仅是最低方法中的变量。 - DrGriff

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接