异步/等待高性能服务器应用程序?
C#5中新的异步/等待关键字看起来非常有前途,但我阅读了一篇关于对这些应用程序的性能影响的文章,因为编译器将为异步方法生成一个相当复杂的状态机。
使用这些关键字的异步编程非常容易,但是与套接字的SocketAsyncEventArgs一样好?
第二个问题:像Stream.WriteAsync这样的异步IO方法真的是异步的(在.Net上的完成端口或Mono上的epoll / poll),还是这些方法将写入调用推送到线程池是便宜的包装?
第三个问题:在UI应用程序的SynchronizationContext旁边,是否有一种方法来实现某种单线程上下文? 就像一个事件循环,以便完成的任务继续在主线程上? 我发现了Nito.AsyncEx库,但我不太确定这是否是我需要的。
async
本身是非常高效的。 一大堆工作进入了这一步。
通常,在服务器端,您关心的是async
I / O。 我将忽略async
CPU绑定方法,因为async
开销无论如何都会在噪声中丢失。
异步I / O会增加每个请求的内存使用量,但它会减少每个请求的线程使用率。 所以你最终获胜(除了临界病理角落病例)。 所有异步I / O都是如此,包括async
。
await
设计了一种模式 - 不仅仅是Task
类型 - 所以如果你需要尽可能多的表现,你可以。
我阅读了一篇关于对这些应用程序的性能影响的文章,因为编译器将为异步方法生成一个相当复杂的状态机。
Stephen Toub阅读的文章非常出色。 我也推荐Async视频的Zen(也由Stephen Toub提供)。
使用这些关键字的异步编程非常容易,但是与套接字的SocketAsyncEventArgs一样好?
首先,了解SocketAsyncEventArgs
更具可扩展性,因为它减少了内存垃圾。 使用async
套接字的更简单方法会产生更多的内存垃圾,但由于await
是基于模式的,因此您可以为SocketAsyncEventArgs
API定义自己的async
兼容包装器(如Stephen Toub的博客中所示...我在此感受到一种模式)。 这可以让你挤出每盎司的表现。
从长远来看,设计一个横向扩展系统通常会更好,而不是扭曲代码以避免少量内存分配。 恕我直言。
第二个问题:像Stream.WriteAsync这样的异步IO方法真的是异步的(在.Net上的完成端口或Mono上的epoll / poll),还是这些方法将写入调用推送到线程池是便宜的包装?
我不知道莫诺。 在.NET上,大多数异步I / O方法都基于完成端口。 Stream
类是一个明显的例外。 Stream
基类默认会做一个“便宜的包装器”,但允许派生类覆盖这种行为。 来自网络通信的数据Stream
总是覆盖这个来提供真正的异步I / O。 Stream
与文件打交道的唯一如果流被明确构建异步I / O覆盖。
第三个问题:在UI应用程序的SynchronizationContext旁边,是否有一种方法来实现某种单线程上下文?
ASP.NET也有一个SynchronizationContext
,所以如果你使用ASP.NET,你已经设置好了。
如果你正在做自己的基于套接字的服务器(例如,一个Win32服务),那么你可以在我的AsyncEx库中使用AsyncContext
类型。 但这听起来不像这是你真正想要的。 AsyncContext
将在当前线程上创建一个单线程上下文。 但是,服务器应用程序async
的真正威力来自于扩展请求而不是线程。
考虑ASP.NET SynchronizationContext
是如何工作的:随着每个请求的进入,它抓取一个线程池线程并构造一个SynchronizationContext
(用于该请求)。 当该请求有异步工作要做时,它向SynchronizationContext
注册,并且运行该请求的线程返回到线程池。 稍后,当异步工作完成时,它会抓取线程池线程(任何线程),在其上安装现有的SynchronizationContext
,并继续处理该请求。 当请求最终完成时,它的SynchronizationContext
被处置。
该进程中的关键是,当请求正在等待( await
)异步操作时,没有专用于该请求的线程。 由于与线程相比,请求相当轻量,因此可以使服务器更好地扩展。
如果您为每个请求提供了单线程的SynchronizationContext
例如AsyncContext
,那么即使它没有任何关系,它也会将线程绑定到每个请求。 这并不比同步多线程服务器更好。
如果你想解决发明你自己的SynchronizationContext
你可以在SynchronizationContext
上找到我的MSDN文章。 我还在那篇文章中介绍了异步方法如何“注册”和“安装”上下文; 这主要是通过async void
自动完成的,并且await
所以你不需要明确地做。
如果你在异步IO的环境中使用它,这是一个有争议的问题。 花在数据库操作,文件/网络IO等上的时间最多只需几毫秒。 如果没有纳秒, async
的开销在最坏情况下将是微秒。 你需要小心的是,当你有很多你正在等待的操作时(比如成千上万,甚至更多),那些操作非常快。 当那些异步操作代表CPU绑定的工作时,使用await
的开销至少是显而易见的。 请注意,就人类理解能力而言,为状态机生成的代码有点复杂,但总体来说,状态机一般表现得相当好。
这些方法不仅仅是阻塞线程池线程的封装,不是。 这会挫败await
代表的目的。 这些方法不会阻塞任何线程,并依靠OS挂钩来完成任务。
当然,你可以创建自己的SynchronizationContext
而不是完全依赖于你的UI框架提供的现有的。 这是一个很好的例子。 使用这样的东西时要小心; 这对于正确的任务来说是一个很好的工具,但可以被滥用来找到一种更加创造性的阻止方式,当你应该只是异步地做所有事情时。