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

  • 优缺点是什么?
  • 对框架有什么建议吗?
  • 对简单/基本教程有什么建议吗?
  • 似乎 epollpoll 是特定于 Linux 的......是否有 Windows 的等效替代方案?
  • 最佳答案

    线程池与 poll 和 epoll 并不真正属于同一类别,因此我假设您将线程池称为“线程池以每个连接一个线程处理多个连接”。

    利弊

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

  • 构架

    libevent -- 2.0版本也支持Windows下的补全端口。

    ASIO -- 如果你在你的项目中使用了 Boost,那就别无所求:你已经有了它作为 boost-asio。

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

    上面列出的框架带有大量文档。 Linux docs 和 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)相反。顾名思义,它们表示完成,而不是准备就绪。也就是说,您触发一个异步请求并忘记它,直到一段时间后您被告知它已完成(成功或不那么成功,也有“立即完成”的异常(exception)情况)。
    使用 epoll,您会一直阻塞,直到您收到“某些数据”(可能只有一个字节)已经到达并且可用或者有足够的缓冲区空间以便您可以在不阻塞的情况下执行写操作的通知。只有这样,您才开始实际操作,然后希望它不会阻塞(除了您所期望的,没有严格的保证——因此将描述符设置为非阻塞并检查 EAGAIN [EAGAIN 和 EWOULDBLOCK对于套接字,因为哦,很高兴,标准允许两个不同的错误值])。

    关于asynchronous - epoll、poll、threadpool 有什么区别?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/4093185/

    10-13 06:56