单线程服务器如何通过非阻塞 I/O 满足多个客户端的需求?

How is a single threaded server able to cater to multiple clients even thru non-blocking I/O?

在实现服务器时,我们可以将一个客户端请求委托给一个线程。我读到这种方法的问题是每个线程都有自己的堆栈,这将非常“昂贵”。另一种方法是让服务器是单线程的,并在这个服务器线程上实现所有客户端请求,并将 I/O 请求作为非阻塞请求。我的疑问是,如果一个服务器线程同时是 运行 多个客户端请求,服务器代码不会有指令指针、局部变量集、每个客户端请求的函数调用堆栈,那么这不会又是“昂贵的”吗? “ 和以前一样。我们如何真正节省?

I read that problem with this approach is that each thread will have its own stack and this would be very "expensive".

取决于您的系统资源有多紧张。典型的 JVM 堆栈 - space 每个线程分配默认为 1mB 在许多当前架构上,尽管这可以使用 -Xss 命令行参数进行调整。您的 JVM 有多少系统内存可供使用以及您需要多少线程决定了您是否愿意为编写单线程服务器付出高昂的代价。

My doubt is that if one server thread is running multiple client requests simultaneously, won't server code have instruction pointer, set of local variables, function calls stacks for each client request, then won't this again be "expensive" as before

它当然需要在堆中存储每个请求的上下文信息,但我怀疑保存传入连接服务所需的变量的信息量少于 1mB。

像大多数事情一样,当我们寻求优化程序时,无论是减少内存还是其他系统资源的使用,我们真正竞争的是代码复杂性。做对更难,维护也更难。 尽管线程程序可能非常复杂,但将请求处理程序隔离在单个线程中可以使代码极其简单,除非它需要以某种方式与其他请求协调。在大多数情况下,编写高性能单线程服务器比线程版本复杂得多。当然,由于不能使用多个处理器,性能也会受到限制。

使用非阻塞 I/O,单个 I/O 线程可以处理多个连接。 I/O 线程将在以下时间收到通知:

  • 客户端想要连接
  • 当上一轮套接字的写缓冲区已满时,连接的套接字的写缓冲区有space。
  • 连接的套接字的读取缓冲区有数据可供读取

因此线程利用事件多路复用来使用选择器同时为连接提供服务。线程等待来自选择器的一组选择键,选择键包含您注册的事件的状态,您可以将 'session' 之类的用户数据附加到选择键。

这里使用的一个非常典型的设计模式是反应器模式。

但通常您希望防止阻塞 I/O 线程的较长 运行 请求。因此,您将工作卸载到不同的线程池。然后reactor变为proactor模式。

而且您通常希望扩展 I/O 线程的数量。所以你可以有一堆 I/O 线程并行。

但是您的应用程序中的线程总数应该保持有限。

这完全取决于你想要什么。以上是我在 Hazelcast 工作时经常使用的技术。

我不会从头开始编写所有这些逻辑。如果你想利用网络,我会看看 Netty。它负责大部分繁重的工作,并内置各种优化。

我不能 100% 确定不写入其堆栈的线程是否会实际消耗 1MB 的物理内存。在 Linux 中,(共享的)零页用于内存分配,因此除非实际写入线程的堆栈,否则不会分配实际的页框(物理内存);这将触发写时复制以进行页框的实际分配。除了节省内存之外,这还可以防止在将堆栈清零时浪费内存带宽。线程的内存消耗是一回事;但是上下文切换是另一个问题。如果您的线程比核心多得多,上下文切换可能会成为一个真正的性能问题。