如果您必须问这个问题,那么您可能不熟悉大多数 Web 应用程序/服务的功能。您可能认为所有软件都这样做:
user do an action
│
v
application start processing action
└──> loop ...
└──> busy processing
end loop
└──> send result to user
但是,这不是 Web 应用程序或任何以数据库作为后端的应用程序的工作方式。 Web 应用程序这样做:
user do an action
│
v
application start processing action
└──> make database request
└──> do nothing until request completes
request complete
└──> send result to user
在这种情况下,软件的大部分运行时间都在使用 0% 的 CPU 时间等待数据库返回。
多线程网络应用:
多线程网络应用程序这样处理上述工作负载:
request ──> spawn thread
└──> wait for database request
└──> answer request
request ──> spawn thread
└──> wait for database request
└──> answer request
request ──> spawn thread
└──> wait for database request
└──> answer request
所以线程大部分时间都在使用 0% CPU 等待数据库返回数据。在这样做的同时,他们必须分配线程所需的内存,其中包括每个线程的完全独立的程序堆栈等。此外,他们将不得不启动一个线程,虽然不像启动一个完整进程那样昂贵,但仍然不完全便宜。
单线程事件循环
既然我们大部分时间都在使用 0% 的 CPU,为什么不在我们不使用 CPU 的时候运行一些代码呢?这样,每个请求仍将获得与多线程应用程序相同的 CPU 时间,但我们不需要启动线程。所以我们这样做:
request ──> make database request
request ──> make database request
request ──> make database request
database request complete ──> send response
database request complete ──> send response
database request complete ──> send response
实际上,这两种方法都以大致相同的延迟返回数据,因为主导处理的是数据库响应时间。
这里的主要优点是我们不需要产生一个新线程,所以我们不需要做很多很多会减慢我们速度的 malloc。
神奇的隐形线程
看似神秘的事情是上述两种方法如何设法“并行”运行工作负载?答案是数据库是线程化的。所以我们的单线程应用实际上是在利用另一个进程的多线程行为:数据库。
单线程方法失败的地方
如果您需要在返回数据之前进行大量 CPU 计算,那么单线程应用程序就会失败。现在,我不是指处理数据库结果的 for 循环。这仍然主要是 O(n)。我的意思是做傅里叶变换(例如 mp3 编码)、光线追踪(3D 渲染)等。
单线程应用程序的另一个缺陷是它只会使用单个 CPU 内核。因此,如果您有一个四核服务器(现在并不罕见),那么您就不会使用其他 3 个核心。
多线程方法失败的地方
如果您需要为每个线程分配大量 RAM,则多线程应用程序会失败。首先,RAM 使用本身意味着您无法处理与单线程应用程序一样多的请求。更糟糕的是,malloc 很慢。分配大量对象(这在现代 Web 框架中很常见)意味着我们最终可能会比单线程应用程序慢。这就是 node.js 通常胜出的地方。
最终使多线程变得更糟的一个用例是当您需要在线程中运行另一种脚本语言时。首先,您通常需要 malloc 该语言的整个运行时,然后您需要 malloc 脚本使用的变量。
因此,如果您使用 C 或 go 或 java 编写网络应用程序,那么线程的开销通常不会太糟糕。如果您正在编写一个 C Web 服务器来服务 PHP 或 Ruby,那么使用 javascript、Ruby 或 Python 编写一个更快的服务器非常容易。
混合方法
一些网络服务器使用混合方法。例如,Nginx 和 Apache2 将其网络处理代码实现为事件循环的线程池。每个线程同时运行一个事件循环,单线程处理请求,但请求在多个线程之间进行负载平衡。
一些单线程架构也使用混合方法。您可以启动多个应用程序,而不是从单个进程启动多个线程 - 例如,四核机器上的 4 个 node.js 服务器。然后,您使用负载平衡器在进程之间分配工作负载。
实际上,这两种方法在技术上是相同的镜像。