事件驱动编程如何帮助只做 IO 的网络服务器?

How does event-driven programming help a webserver that only does IO?

我正在为我们的新后端项目考虑一些 frameworks/programming 方法。它涉及聚合下游服务的 BackendForFrontend 实现。为简单起见,这些是它经过的步骤:

  1. 请求进入网络服务器
  2. 网络服务器发出下游请求
  3. 下游请求return结果
  4. 网络服务器return请求

事件驱动编程比 "regular" 每个请求线程处理有何优势?一些网站试图解释,通常归结为这样:

The second solution is a non-blocking call. Instead of waiting for the answer, the caller continues execution, but provides a callback that will be executed once data arrives.

我不明白的是:我们需要一个 thread/handler 来等待这个数据,对吧?事件处理程序可以继续很好,但我们仍然需要(在此示例中)每个请求等待每个下游请求的 thread/handler,对吧?

考虑这个例子:下游请求需要 n 秒才能到达 return。在这 n 秒内,有 r 个请求。在 thread-per-request 中,我们需要 r 个线程:每个请求一个。 n 秒后,第一个线程完成处理并可用于新请求。

在实现事件驱动设计时,我们需要 r+1 个线程:一个事件循环和 r 个处理程序。每个处理程序接受一个请求,执行它,并在完成后调用回调。

那么这对事情有何改善?

What I don't understand: we need a thread/handler to await this data, right?

不是真的。 NIO 背后的想法是没有线程会被阻塞。

这很有趣,因为操作系统已经以 non-blocking 方式运行。我们的编程语言是以阻塞方式建模的。

举个例子,假设您有一台带有单个 CPU 的计算机。您执行的任何 I/O 操作都会比 CPU 慢几个数量级,对吗?假设你想读取一个文件。你认为 CPU 会留在那里,空闲,什么都不做,而磁盘磁头去获取几个字节并将它们放入磁盘缓冲区?明显不是。操作系统将注册一个中断(即回调),同时将有价值的 CPU 用于其他事情。当磁头成功读取到几个字节并可供消费时,将触发中断,OS 将关注它,恢复之前的进程块并分配一些 CPU 处理可用数据的时间。

因此,在这种情况下,CPU 就像您应用程序中的一个线程。它永远不会被阻止。它总是在做一些 CPU-bound 的事情。

NIO 编程背后的思想是相同的。在您公开的情况下,假设您的 HTTP 服务器只有一个线程。当您收到来自客户的请求时,您需要发出上游请求(代表 I/O)。所以 NIO 框架在这里要做的是发出请求并在响应可用时注册回调。

在那之后,您宝贵的单线程立即被释放以处理另一个请求,该请求将注册另一个回调,依此类推。

回调解决后,将自动安排由您的单线程处理。

因此,该线程作为一个事件循环工作,您应该只在其中安排 CPU 绑定的东西。每次你需要做 I/O 时,都是以 non-blocking 的方式完成的,当 I/O 完成时,一些 CPU-bound 回调被放入事件循环来处理响应。

这是一个强大的概念,因为使用非常少量的线程,您可以处理数千个请求,因此您可以更轻松地扩展。事半功倍。

此功能是 Node.js 的主要卖点之一,也是即使使用单线程也可用于开发后端应用程序的原因。

同样,这也是 Netty, RxJava, Reactive Streams Initiative and the Project Reactor 等框架激增的原因。他们都在寻求推广这种优化和编程模型。

还有一个有趣的新框架运动,它们利用这一强大的功能并试图相互竞争或互补。我说的是像 Vert.x and Ratpack 这样有趣的项目。而且我很确定还有更多其他语言。

非阻塞范式的整个思想是通过这个称为 "Event Loop"

有趣的参考资料:

  1. http://www.masterraghu.com/subjects/np/introduction/unix_network_programming_v1.3/ch06lev1sec2.html
  2. Understanding the Event Loop
  3. https://www.youtube.com/watch?v=8aGhZQkoFbQ