8

我最近开始学习 node.js,这是一个基于 V8 的 JavaScript 库,以其非阻塞 IO 和令人难以置信的速度而闻名。

据我了解,节点不会等待 IO 响应,而是运行一个事件循环(类似于游戏循环),不断检查未完成的操作并在 IO 响应后立即继续/完成它们。节点性能与 Apache HTTPD 相比,节点速度明显更快,同时使用更少的内存。

现在,如果您阅读有关 Apache 的信息,您会了解到它每个用户使用 1 个线程,这可能会显着减慢它的速度,这就是我的问题出现的地方:

如果将线程与节点在其事件循环中内部执行的操作进行比较,您会开始看到相似之处:两者都是等待资源响应的未完成进程的抽象,都检查操作是否定期进行,然后不占用CPU 一段时间(至少我认为一个好的阻塞 API 在重新检查之前会休眠几毫秒)。

现在,使线程变得如此糟糕的显着关键差异在哪里?

4

2 回答 2

10

这里的区别是上下文切换。操作系统交换线程需要:

  • 保存指令指针(由 CPU 完成)
  • 保存 CPU 寄存器(如果线程进行了阻塞调用,则可能不需要,但如果它被抢占,则需要)
  • 交换调用堆栈。即使堆栈驻留在相同的虚拟内存空间中,这也至少是一次写入和一些读取,甚至适用于微线程(光纤)。
  • 在交换到不同进程的情况下,交换到内核模式,更新虚拟内存表并返回用户模式。

在事件队列的情况下:

  • 状态已更新。这在任何情况下都需要发生。
  • 事件处理程序返回。不是交换调用堆栈,而是弹出当前调用堆栈。
  • 检查事件队列是否有待处理的请求。只有当没有挂起的请求时,应用才会等待。这可以通过重复睡眠(如 OP 所建议)或(更好)通过对事件队列进行阻塞调用来完成。如果事件队列(例如一组 TCP 套接字)由操作系统管理,那么操作系统负责通知应用程序有新事件(一个套接字可以接受更多数据)。

如果服务器负载很高,事件队列的唯一开销是处理程序返回、读取队列和处理程序调用。在线程化方法中,交换线程会产生额外的开销。

此外,正如PST所提到的,线程方法引入了对锁定的需求。锁定本身很便宜,但等待其他线程释放资源需要额外的上下文切换,因为等待线程无法继续。甚至有可能一个线程被换入以获取锁,但在几个时钟周期后才被换出,因为它还需要锁定另一个资源。比较操作系统完成了多少工作(至少读取线程队列和交换调用堆栈)与线程完成了多少工作(从一个调用返回并进行另一个调用)。

于 2012-10-28T03:44:19.840 回答
0

从一个方面来看,它确实取决于特定于该语言的线程的实现。然而,一般来说,成本高的是线程的创建,而不是线程的运行。因此,某些语言(如 .Net)保留了一个线程池,因此您可以获取一个基本上已经创建的线程池,从而降低成本。

一位教授告诉我,线程的问题还在于,每种语言都有相当于 Thread.Yield() 函数,但实际上没有人使用它;因此,您将遇到的每个线程在调度方面都非常激进,这会在互斥锁和信号量之间引发各种战争;由于所使用的攻击性级别,某些线程从未真正运行过,这本身就是一个问题。

线程的好处是它们以增加功能为代价从其他循环(如 GUI 循环)中卸载功能。据我所知,事件仍然在单个线程中运行(除非特别告知要这样做)。

于 2012-10-28T03:32:43.280 回答