异步等待功能使编写非阻塞代码变得优雅。但是,虽然是非阻塞的,但在异步函数中执行的工作仍然很重要。
在编写异步代码时,我发现编写遵循“一直到兔子洞”模式的代码是很自然的,可以这么说,调用树中的所有方法都标记为异步并且使用的 API 是异步的;但即使在非阻塞的情况下,执行的代码也会占用相当多的上下文线程时间。
您如何以及何时决定在异步之上同时运行支持异步的方法?是否应该在调用树中创建更高或更低的新任务?这种“优化”有什么最佳实践吗?
异步等待功能使编写非阻塞代码变得优雅。但是,虽然是非阻塞的,但在异步函数中执行的工作仍然很重要。
在编写异步代码时,我发现编写遵循“一直到兔子洞”模式的代码是很自然的,可以这么说,调用树中的所有方法都标记为异步并且使用的 API 是异步的;但即使在非阻塞的情况下,执行的代码也会占用相当多的上下文线程时间。
您如何以及何时决定在异步之上同时运行支持异步的方法?是否应该在调用树中创建更高或更低的新任务?这种“优化”有什么最佳实践吗?
我已经async
在生产中使用了几年。我推荐一些核心“最佳实践”:
async
代码。使用async
“一直向下”。(推论:除非你必须使用async Task
,否则更喜欢)。async void
async void
ConfigureAwait(false)
在您的“库”方法中尽可能使用。您已经弄清楚了“async
一直向下”的部分,并且您正处于ConfigureAwait(false)
变得有用的地步。
假设您有一个调用另一个async
方法的方法。使用 的结果更新 UI ,但不依赖于 UI。所以我们有:A
async
B
A
B
B
async Task A()
{
var result = await B();
myUIElement.Text = result;
}
async Task<string> B()
{
var rawString = await SomeOtherStuff();
var result = DoProcessingOnRawString(rawString);
return result;
}
在此示例中,我将调用B
“库”方法,因为它实际上并不需要在 UI 上下文中运行。现在,B
确实在 UI 线程中运行,因此DoProcessingOnRawString
会导致响应性问题。
因此,在ConfigureAwait(false)
每个await
in 中添加一个B
:
async Task<string> B()
{
var rawString = await SomeOtherStuff().ConfigureAwait(false);
var result = DoProcessingOnRawString(rawString);
return result;
}
现在,当在ingB
之后恢复时(假设它确实必须),它将在线程池线程而不是 UI 上下文上恢复。完成后,即使它在线程池上运行,也会在 UI 上下文中恢复。await
SomeOtherStuff
await
B
A
您无法添加ConfigureAwait(false)
,A
因为A
取决于 UI 上下文。
您还可以选择将任务显式排队到线程池 ( await Task.Run(..)
),如果您有特定的 CPU 密集型功能,您应该这样做。但是,如果您的性能受到“数千次剪纸”的影响,您可以使用ConfigureAwait(false)
将大量async
“家务”卸载到线程池中。
您可能会发现我的介绍性帖子很有帮助(它涉及更多“为什么”),并且async
常见问题解答也有很多很好的参考资料。
Async-await实际上并不使用当前 .NET 进程空间中的线程。它是为“阻塞” IO 和网络操作而设计的,例如数据库调用、Web 请求、一些文件 IO。
我无法理解 C# 对你所说的兔子洞技术有什么好处。这样做只会使代码变得模糊,并且不必要地将潜在的高 CPU 代码与 IO 代码耦合。
为了直接回答你的问题,我只会在前面提到的 IO/网络场景中使用 async-await,就在你正在执行阻塞操作的地方,对于任何受 CPU 限制的事情,我会使用线程技术来充分利用可用的 CPU 内核数。无需将这两个问题混为一谈。