epoll,poll,threadpool有什么区别?


76

可能有人解释的区别是什么之间epollpoll和线程池?

  • 优点/缺点是什么?
  • 对框架有什么建议吗?
  • 对简单/基础教程有什么建议吗?
  • 似乎epoll并且poll特定于Linux ... Windows是否有等效的替代品?

Answers:


217

线程池与民意测验和epoll确实不属于同一类别,因此我假设您所指的线程池与“线程池以每个连接处理一个线程的方式处理多个连接”中的线程池相同。

利弊

  • 线程池
    • 对于中小型并发来说,合理的效率甚至可以胜过其他技术。
    • 利用多个核心。
    • 即使某些系统(例如Linux)原则上可以调度10万个线程,也无法将扩展范围远远超过“几百个”。
    • 天真的实现存在“雷群”问题。
    • 除了上下文切换和打雷群之外,还必须考虑内存。每个线程都有一个堆栈(通常至少为一个兆字节)。因此,一千个线程仅将一个GB的RAM用于堆栈。即使未提交该内存,在32位操作系统下,它仍会占用相当大的地址空间(在64位操作系统下,这并不是真正的问题)。
    • 线程可以实际使用epoll,尽管显而易见的方法(所有线程都在上阻塞epoll_wait)是没有用的,因为epoll会唤醒等待它的每个线程,因此仍然会有相同的问题。
      • 最佳解决方案:单线程侦听epoll,执行输入多路复用,并将完整的请求交给线程池。
      • futex是您的朋友吗,例如每个线程一个快速转发队列。尽管文档记录不清且笨拙,但futex它确实提供了所需的内容。epoll可能一次返回多个事件,并futex让您高效且以精确控制的方式一次唤醒N个阻塞的线程(min(num_cpu, num_events)理想情况下为N ),并且在最佳情况下,它根本不涉及额外的syscall / context开关。
      • 实施并非易事,请多加注意。
  • fork (又名旧时尚线程池)
    • 对于中小型并发来说,效率相当高。
    • 扩展规模远不超过“几百个”。
    • 上下文切换昂贵得多(不同的地址空间!)。
    • 在较旧的系统上,可伸缩性会更差,而fork的成本要高得多(所有页面的深拷贝)。即使在现代系统fork上也不是“免费”的,尽管开销大部分是由写时复制机制合并的。在也被修改的大型数据集上,随后的大量页面错误fork可能会对性能产生负面影响。
    • 但是,事实证明可以可靠地工作30多年。
    • 易于实现且坚如磐石:如果任何进程崩溃,世界将不会终结。(几乎)您无能为力。
    • 很容易出现“雷声群”。
  • poll / select
    • 两种味道(BSD与System V)大致相同。
    • 用法有些过时和缓慢,有些笨拙,但是实际上没有平台不支持它们。
    • 等到一组描述符上发生“某事”
      • 允许一个线程/进程一次处理多个请求。
      • 没有多核使用。
    • 每次等待时,都需要将描述符列表从用户复制到内核空间。需要对描述符执行线性搜索。这限制了它的有效性。
    • 不能很好地扩展到“数千”(实际上,在大多数系统上,硬限制大约为1024,而在某些系统上则低至64)。
    • 使用它是因为它是可移植的,如果您仅处理十几个描述符(那里没有性能问题),或者您必须支持没有更好的平台的话。请勿使用其他方式。
    • 从概念上讲,服务器要比分支服务器复杂一些,因为您现在需要维护许多连接,并且每个连接都需要一个状态机,并且必须在请求进入时进行多路复用,组合部分请求等。服务器只知道一个套接字(嗯,两个,计算正在监听的套接字),读取直到它具有所需的内容或连接半关闭,然后写入所需的内容。它不必担心阻塞,就绪或饥饿,也不必担心会引入一些无关的数据,这是其他流程的问题。
  • epoll
    • 仅Linux。
    • 昂贵的修改与有效的等待的概念:
      • 添加描述符时,将有关描述符的信息复制到内核空间(epoll_ctl
        • 通常这很少发生。
      • 难道不是等待事件时,需要将数据复制到内核空间(epoll_wait
        • 这通常是经常发生的事情。
      • 将服务员(或更确切地说,其epoll结构)添加到描述符的等待队列中
        • 因此,描述符知道谁在监听,并在适当时直接向服务员发送信号,而不是让服务员搜索描述符列表
        • 相反的poll工作方式
        • 相对于描述符数量,k(非常快)为k的O(1),而不是O(n)
    • timerfd和搭配使用时效果很好eventfd(计时器分辨率和准确性也很惊人)。
    • 与配合良好signalfd,消除了对信号的笨拙处理,使它们以非常优雅的方式成为常规控制流的一部分。
    • 一个epoll实例可以递归托管其他epoll实例
    • 此编程模型所做的假设:
      • 大多数描述符大部分时间都是空闲的,很少有事情(例如“数据接收”,“连接已关闭”)实际上发生在少数描述符上。
      • 大多数时候,您不想从集合中添加/删除描述符。
      • 大多数情况下,您正在等待某些事情发生。
    • 一些小陷阱:
      • 通过级别触发的epoll唤醒等待它的所有线程(这是“按预期工作”),因此,将epoll与线程池一起使用的幼稚方式是无用的。至少对于TCP服务器而言,这不是什么大问题,因为无论如何都必须首先组装部分请求,因此,幼稚的多线程实现不会以任何方式进行。
      • 不能像人们期望的那样进行文件读/写(“始终准备就绪”)。
      • 直到最近才可以与AIO一起使用,现在可以通过来使用eventfd,但需要(迄今为止)未记录的功能。
      • 如果上述假设成立,则epoll可能效率低下,并且效果poll可能相同或更好。
      • epoll不能做“魔术”,也就是说,相对于发生事件数,仍然必须为O(N)。
      • 但是,epoll由于新的recvmmsgsyscall一次返回多个准备就绪通知(尽可能多,直到您指定为maxevents),因此可以很好地发挥作用。这样就可以在繁忙的服务器上通过一个syscall接收到15条EPOLLIN通知,并通过第二个syscall读取对应的15条消息(syscall减少了93%!)。不幸的是,一次recvmmsg调用的所有操作都引用同一个套接字,因此它对于基于UDP的服务最有用(对于TCP,将必须有一种recvmmsmsgsyscall,每项还需要一个套接字描述符!)。
      • 描述符应始终设置为非阻塞,EAGAIN即使在使用时epoll也应进行检查,因为在某些特殊情况下,epoll报告准备情况和后续的读取(或写入)操作仍会阻塞。在某些内核上,poll/也是如此select(尽管它已被修复)。
      • 通过幼稚的实现,慢速发送者可能会饿死。当盲目读取直到EAGAIN收到通知后返回时,可以无限地从快速发送者读取新的传入数据,而完全饿死了慢速发送者(只要数据保持足够快的速度,您可能会EAGAIN在很长一段时间内看不到! )。以同样的方式适用于poll/ select
      • 在某些情况下,由于文档(手册页和TLPI)含糊不清(“可能”,“应该”,“可能”),并且有时会误导其操作,因此边缘触发模式在某些情况下会有一些怪癖和意外行为。
        文档指出,正在等待一个epoll的多个线程都已发出信号。它进一步指出,通知会告诉您自上次调用epoll_wait以来(或从打开描述符开始,如果没有先前调用),是否发生了IO活动。
        在边沿触发模式的真正的,可观察的行为更接近“醒来的第一个已调用线程epoll_wait,这表明由于IO活动已经发生了过去叫 epoll_wait 描述符上的读/写功能,此后仅向调用或已阻塞的下一个线程 再次报告准备情况,以防止有人在描述符上调用读(或写)功能epoll_wait后发生任何操作。” ,也...文档中所建议的不完全是。
  • kqueue
    • BSD类似epoll,不同的用法,效果相似。
    • 在Mac OS X上也可以使用
    • 据说速度更快(我从没使用过,所以无法确定是不是真的)。
    • 注册事件并在单个syscall中返回结果集。
  • IO完成端口
    • Windows的Epoll,或者类固醇的Epoll。
    • 与可以以某种方式等待或警报的所有事物(套接字,等待计时器,文件操作,线程,进程)无缝协作
    • 如果微软在Windows上正确地解决了一件事,那就是完成端口:
      • 开箱即用,无需担心任何数量的线程
      • 没有雷声群
      • 按LIFO顺序逐个唤醒线程
      • 保持缓存温暖并最大程度地减少上下文切换
      • 尊重机器上的处理器数量或提供所需数量的工人
    • 允许应用程序发布事件,从而使其非常简单,故障保护和高效的并行工作队列实现(在我的系统上每秒调度500,000个任务以上)。
    • 较小的缺点:一旦添加,就不容易删除文件描述符(必须关闭并重新打开)。

构架

libevent -2.0版本还支持Windows下的完成端口。

ASIO-如果您在项目中使用Boost,则别无所求:您已经可以将其作为boost-asio使用。

对简单/基础教程有什么建议吗?

上面列出的框架带有大量的文档。Linux文档和MSDN广泛解释了epoll和完成端口。

使用epoll的迷你教程:

int my_epoll = epoll_create(0);  // argument is ignored nowadays

epoll_event e;
e.fd = some_socket_fd; // this can in fact be anything you like

epoll_ctl(my_epoll, EPOLL_CTL_ADD, some_socket_fd, &e);

...
epoll_event evt[10]; // or whatever number
for(...)
    if((num = epoll_wait(my_epoll, evt, 10, -1)) > 0)
        do_something();

IO完成端口的迷你教程(请注意使用不同的参数两次调用CreateIoCompletionPort):

HANDLE iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0); // equals epoll_create
CreateIoCompletionPort(mySocketHandle, iocp, 0, 0); // equals epoll_ctl(EPOLL_CTL_ADD)

OVERLAPPED o;
for(...)
    if(GetQueuedCompletionStatus(iocp, &number_bytes, &key, &o, INFINITE)) // equals epoll_wait()
        do_something();

(这些小型工具忽略了所有类型的错误检查,希望我没有做任何错别字,但在大多数情况下它们应该可以使您有所了解。)

编辑:
请注意,完成端口(Windows)在概念上可以作为epoll(或kqueue)以其他方式工作。顾名思义,它们表示完成而不是准备。也就是说,您触发一个异步请求,然后将其忽略,直到一段时间后,您被告知该请求已完成(成功或不十分成功,并且还有“立即完成”的例外情况)。
使用epoll,您将阻塞直到通知您“某些数据”(可能只有一个字节)到达并且可用,或者有足够的缓冲区空间,因此您可以进行写操作而不会阻塞。只有到那时,您才开始实际操作,然后希望该操作不会阻塞(除了您期望的那样,没有严格的保证—因此,将描述符设置为非阻塞并检查EAGAIN [EAGAINEWOULDBLOCK是一个好主意。对于套接字,因为很高兴,所以标准允许两个不同的错误值])。


1
我不同意您关于I / O完成端口是MS做对的一件事的说法。很高兴您在编辑中注意到了它的落后设计!
马特

好答案(+1)。但是,您是指min(num_cpu, num_events)“ futex”描述中的意思吗?
Nemo

@Nemo:您当然是对的,一定是min,不是max-我会解决错字。谢谢。
2013年

1
实际上,我已经对此有所改变。在使用RDMA之后,IOCP API更适合该模型。潜在的性能会更好。实际上,我不太确定。无论如何...我不会说它不再落后,只是有所不同,而且要努力向前发展也要困难得多。
马特

我喜欢您提供的所有细节。我认为EPOLLET仍然唤醒所有线程。fs / eventpoll.c:ep_send_events_proc()是唯一使用该标志的函数,并且仅用于确定是否应将其重新插入就绪列表。
Ant Manelope
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.