如果您必须问这个问题,那么您可能不熟悉大多数Web应用程序/服务的功能。您可能会认为所有软件都可以这样做:
user do an action
│
v
application start processing action
└──> loop ...
└──> busy processing
end loop
└──> send result to user
但是,这不是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通常获胜的地方。
一个最终使多线程变得更糟的用例是,当您需要在线程中运行另一种脚本语言时。首先,通常需要为该语言分配整个运行时,然后需要分配脚本使用的变量。
因此,如果您使用C或go或java编写网络应用程序,则线程的开销通常不会太糟。如果您要编写C Web服务器来服务PHP或Ruby,那么用javascript,Ruby或Python编写速度更快的服务器非常容易。
混合方式
某些Web服务器使用混合方法。例如,Nginx和Apache2将其网络处理代码实现为事件循环的线程池。每个线程运行一个事件循环,同时处理单线程请求,但请求在多个线程之间进行负载平衡。
一些单线程体系结构也使用混合方法。您可以启动多个应用程序,而不是从单个进程启动多个线程,例如,在四核计算机上启动4个node.js服务器。然后,您可以使用负载平衡器在各个进程之间分配工作负载。
实际上,这两种方法在技术上是彼此相同的镜像。