民意测验,民意测验,线程池有什么区别?

有人能解释一下 epollpoll和线程池之间的区别吗?

  • 优缺点是什么?
  • 对框架有什么建议吗?
  • 对于简单/基本的教程有什么建议吗?
  • 看起来 epollpoll都是 Linux 特有的... 对于 Windows 来说,有没有一个等价的替代品?
17500 次浏览

Threadpool 实际上并不像 poll 和 epoll 那样属于同一类别,因此我假设您所指的 Threadpool 是“用每个连接一个线程处理多个连接的 Threadpool”。

利与弊

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

框架

Libevent ——2.0版本还支持 Windows 下的完成端口。

ASIO ——如果您在项目中使用 Boost,那么不用再进一步查看: 您已经有了这个可用的 ost-ASIO。

对于简单/基本的教程有什么建议吗?

上面列出的框架提供了大量的文档。

使用 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 [ EAGAIN 还有 EWOULDBLOCK for socket,因为这个标准允许两个不同的错误值]是一个好主意)。