【问题标题】:async/await for high performance server applications?异步/等待高性能服务器应用程序?
【发布时间】:2013-02-06 21:51:56
【问题描述】:

C# 5 中新的 async/await 关键字看起来很有前景,但我阅读了一篇关于对这些应用程序的性能影响的文章,因为编译器将为异步方法生成一个相当复杂的状态机。

使用这些关键字进行异步编程要容易得多,但它和 SocketAsyncEventArgs for Sockets 一样好吗?

第二个问题:像 Stream.WriteAsync 这样的异步 IO 方法真的是异步的吗(.Net 上的 Completion Ports 或 Mono 上的 epoll/poll)或者这些方法是用于将写入调用推送到线程池的廉价包装器?

第三个问题:除了 UI 应用程序的 SynchronizationContext 之外,有没有办法实现某种单线程上下文?类似于事件循环的东西,以便完成的任务在主线程上继续? 我发现了 Nito.AsyncEx 库,但我不太确定这是否是我需要的。

【问题讨论】:

标签: c# sockets asynchronous async-await


【解决方案1】:

async 本身的性能非常好。为此做了很多工作。

一般来说,在服务器端,您关心的是async I/O。我将忽略async CPU-bound 方法,因为async 开销无论如何都会在噪音中消失。

异步 ​​I/O 会增加每个请求的内存使用量,但会减少每个请求的线程使用量。所以你最终会赢(边缘病态的极端案例除外)。这适用于所有异步 I/O,包括 async

await 被设计成一个模式 - 不仅仅是 Task 类型 - 所以如果你需要尽可能多地挤出性能,你可以。

我阅读了一篇关于对这些应用程序的性能影响的文章,因为编译器将为异步方法生成一个相当复杂的状态机。

Stephen Toub 的 article you read 非常棒。我还推荐 Zen of Async video(也是 Stephen Toub 的)。

使用这些关键字进行异步编程要容易得多,但它和 SocketAsyncEventArgs for Sockets 一样好吗?

首先,了解SocketAsyncEventArgs 更具可扩展性,因为它减少了内存垃圾。使用async 套接字的更简单方法会产生更多内存垃圾,但由于await 是基于模式的,您可以使用define your own async-compatible wrappers for the SocketAsyncEventArgs API(如Stephen Toub 的博客中所见......我在这里感觉到了一种模式;)。这使您可以充分发挥性能。

尽管从长远来看,设计横向扩展系统通常比扭曲代码以避免一些内存分配更好。恕我直言。

第二个问题:像 Stream.WriteAsync 这样的异步 IO 方法真的是异步的吗(.Net 上的 Completion Ports 或 Mono 上的 epoll/poll)或者这些方法是用于将写入调用推送到线程池的廉价包装器?

我不知道 Mono。在 .NET 上,大多数 异步 I/O 方法基于完成端口。 Stream 类是一个明显的例外。 Stream 基类默认会做一个“廉价包装”,但允许派生类覆盖这个行为。来自网络通信的Streams 总是覆盖它以提供真正的异步 I/O。处理文件的Streams 仅覆盖此如果流是为异步 I/O 显式构造的。

第三个问题:除了 UI 应用的 SynchronizationContext 之外,有没有办法实现某种单线程上下文?

ASP.NET 也有一个SynchronizationContext,所以如果您使用的是 ASP.NET,那么您已经设置好了。

如果您正在做自己的基于套接字的服务器(例如,Win32 服务),那么您可以在我的 AsyncEx 库中使用 AsyncContext 类型。但听起来这并不是你真正想要的。 AsyncContext 将在当前线程上创建一个单线程上下文。但是,async 对于服务器应用程序的真正威力来自扩展请求,而不是线程

考虑 ASP.NET SynchronizationContext 的工作原理:当每个 request 进来时,它会抓取一个线程池线程并构造一个 SynchronizationContext(针对那个 request )。当该请求有异步工作要做时,它向SynchronizationContext 注册,并且运行该请求的线程返回到线程池。稍后,当异步工作完成时,它会抓取一个线程池线程(any 线程),在其上安装现有的SynchronizationContext,并继续处理该请求。当请求最终完成时,其SynchronizationContext 被释放。

该过程的关键在于,当请求等待 (await) 异步操作时,没有 个线程专用于该请求。由于 requestthread 相比是相当轻量级的,这使得服务器能够更好地扩展。

如果你给你的每个请求一个单线程的SynchronizationContext,比如AsyncContext,这会为每个请求绑定一个线程,即使它没有任何关系。这几乎不比同步多线程服务器好。

如果您想创建自己的SynchronizationContext,您可能会发现我的MSDN article on SynchronizationContext 很有用。在那篇文章中,我还介绍了异步方法如何“注册”和“安装”上下文;这大部分是由async voidawait 自动完成的,因此您不必明确地执行此操作。

【讨论】:

  • 非常好的答案。不知道大家是否熟悉Netty(一个Java网络框架)。每个客户端在线程池中获得其专用线程,根据可用的处理器线程,最多有 4 - 8 个线程,以避免上下文切换。如果一个线程压力太大,客户端会重新排列,但这种情况很少见。理论上,这种上下文切换应该对可扩展性产生影响,不是吗?你已经让我相信 async/await 对于我的目的来说已经绰绰有余了,但这只是为了我的兴趣。
  • 经过大量测试,我认为是 async/await impl。 .NET4.5 相当惊人。实际上,*Async 方法的性能几乎总是与 SocketAsyncEventArgs 一样好。当然,我的测试有点原始,但我认为它显示了趋势。
【解决方案2】:
  1. 如果您在异步 IO 的上下文中使用它,这是一个有争议的问题。花费在数据库操作、文件/网络 IO 等上的时间最多只有几毫秒。 async 的开销在最坏的情况下将是微秒,如果没有纳秒的话。您需要小心的地方是当您有大量等待的操作(如数千、数万或更多)并且这些操作非常快时.当这些异步操作代表 CPU 密集型工作时,使用await 的开销肯定可能至少是显而易见的。请注意,就人类的理解能力而言,为状态机生成的代码有些复杂,但状态机总体上往往表现得相当好。

  2. 这些方法不仅仅是阻塞线程池线程的包装器,不。这将违背await 所代表的目的。这些方法不会阻塞任何线程,而是依靠操作系统挂钩来完成任务。

  3. 当然,您可以创建自己的SynchronizationContext,而不是完全依赖您的 UI 框架提供的现有的。 Here 就是一个很好的例子。使用这样的东西时要小心;它是完成正确任务的好工具,但当您应该只异步执行所有操作时,它可能会被滥用以找到一种更具创造性的阻塞方式。

【讨论】:

  • 关于 1.: 假设我们有很多客户端(可能是 2000 - 7000 个),主要执行 IO 绑定操作,例如读/写文件或访问数据库。如此大量的 io 绑定任务在理论上可行吗?或者这对于大型服务器来说通常是一种糟糕的模式?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-07-15
  • 1970-01-01
  • 1970-01-01
  • 2013-07-27
  • 1970-01-01
  • 2011-01-29
相关资源
最近更新 更多