如果你不得不问这个问题,那么你可能不熟悉大多数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编写一个更快的服务器是非常容易的。
混合方法
一些web服务器使用混合方法。例如,Nginx和Apache2将网络处理代码实现为事件循环的线程池。每个线程运行一个事件循环,同时处理单线程请求,但请求在多个线程之间是负载平衡的。
一些单线程架构也使用混合方法。而不是从一个进程启动多个线程,你可以启动多个应用程序-例如,在四核机器上的4个node.js服务器。然后使用负载均衡器将工作负载分散到各个进程中。node.js中的cluster模块正是这样做的。
实际上,这两种方法在技术上是彼此相同的镜像。