为什么线程如此昂贵,事件驱动的非阻塞IO在基准测试中表现更好

时间:2012-10-28 03:10:49

标签: multithreading event-driven event-driven-design

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

据我所知,节点不会等待IO响应,而是运行一个事件循环(类似于游戏循环),它会一直检查未完成的操作,并在IO响应时继续/完成它们。将节点性能与Apache HTTPD进行比较,节点使用较少的内存时速度明显加快。

现在,如果你阅读了关于Apache的内容,你会发现它每个用户使用1个线程,这可能会大大降低它的速度,这就是我的问题所在:

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

现在哪个引人注目的关键差异让线程变得更糟?

2 个答案:

答案 0 :(得分:10)

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

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

如果是事件队列:

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

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

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

答案 1 :(得分:0)

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

从教授告诉我的话,线程的问题也在于每个语言都具有Thread.Yield()函数的等价物,但实际上并没有人使用它;因此,您将遇到的每个线程在调度方面都非常积极,这会在互斥锁和sempaphores之间建立各种各样的战争;一些线程,由于使用的攻击程度,从未实际运行,这本身就是一个问题。

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