【问题标题】:What's the difference between epoll, poll, threadpool?epoll、poll、threadpool有什么区别?
【发布时间】:2011-05-04 20:07:31
【问题描述】:

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

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

【问题讨论】:

    标签: asynchronous epoll io-completion-ports


    【解决方案1】:

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

    优点和缺点

    • 线程池
      • 对中小型并发相当有效,甚至可以胜过其他技术。
      • 利用多个内核。
      • 尽管某些系统(例如 Linux)原则上可以很好地调度 100,000 个线程,但扩展性不会超过“数百个”。
      • 简单的实现会出现“thundering herd”问题。
      • 除了上下文切换和雷鸣般的羊群之外,还必须考虑记忆。每个线程都有一个堆栈(通常至少一个兆字节)。因此,一千个线程仅占用一千兆字节的 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 与 System 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 也是如此(尽管它可能已被修复)。
        • 使用 naive 实现,慢速发送者的饥饿是可能的。当盲读直到收到通知返回 EAGAIN 时,可能会无限期地从快速发送方读取新传入数据,同时完全饿死慢速发送方(只要数据保持足够快,您可能看不到 @987654358 @ 有一会儿!)。以同样的方式应用于poll/select
        • 边缘触发模式在某些情况下会出现一些怪癖和意外行为,因为文档(手册页和 TLPI)含糊不清(“可能”、“应该”、“可能”)并且有时会对其操作产生误导。 文档指出,在一个 epoll 上等待的多个线程都已发出信号。它进一步指出,通知会告诉您自上次调用 epoll_wait 以来是否发生了 IO 活动(或者自打开描述符以来,如果没有先前的调用)。
          边缘触发模式下真实的、可观察到的行为更接近于“唤醒调用epoll_wait第一个 线程,表明IO 活动自任何人 上次调用以来已经发生要么 epoll_wait 要么 描述符上的读/写函数,然后只向下一个调用线程或已经阻塞的线程再次报告准备情况 epoll_wait,对于在 anyone 调用描述符上的读取(或写入)函数之后发生的任何操作”。这也有点道理......这与文档所暗示的不完全一样。
    • kqueue
      • BSD类似于epoll,用法不同,效果相似。
      • 也适用于 Mac OS X
      • 传闻更快(我没用过,不知道是不是真的)。​​
      • 注册事件并在单个系统调用中返回结果集。
    • IO 完成端口
      • Windows 的 Epoll,或者更确切地说是类固醇上的 epoll。
      • 所有可等待或以某种方式发出警报的事物无缝协作(套接字、可等待计时器、文件操作、线程、进程)
      • 如果 Microsoft 在 Windows 中做对了一件事,那就是完成端口:
        • 使用任意数量的线程,开箱即用无忧
        • 没有雷声
        • 按 LIFO 顺序一一唤醒线程
        • 保持缓存温暖并最大限度地减少上下文切换
        • 尊重机器上的处理器数量或提供所需数量的工人
      • 允许应用程序发布事件,这有助于实现非常简单、故障安全且高效的并行工作队列实施(在我的系统上每秒安排超过 500,000 个任务)。
      • 次要缺点:添加后不容易删除文件描述符(必须关闭并重新打开)。

    框架

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

    ASIO -- 如果您在项目中使用 Boost,请不要再犹豫:您已经​​将其作为 boost-asio 提供。

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

    上面列出的框架附带大量文档。 Linuxdocs 和 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 [EAGAIN and EWOULDBLOCK 用于套接字,因为天哪,标准允许两个不同的错误值])。

    【讨论】:

    • 我不同意你关于 I/O 完成端口是 MS 做对的一件事的说法。很高兴您在编辑中注意到它的落后设计!
    • 不错的答案 (+1)。但是您是指“futex”描述中的min(num_cpu, num_events) 吗?
    • @Nemo:你当然是对的,必须是min,而不是max——我会改正错字。谢谢。
    • 实际上我已经改变了一些看法。在使用 RDMA 之后,IOCP API 更适合该模型。潜在的性能更好。在实践中,我不太确定。无论如何......我不会说它已经落后了,只是不同,而且更难理解。
    • 我喜欢你提供的所有细节。我认为 EPOLLET 仍然唤醒所有线程。 fs/eventpoll.c: ep_send_events_proc() 是唯一使用该标志的函数,并且仅用于确定它是否应该插入回就绪列表中。
    猜你喜欢
    • 2010-11-01
    • 2020-01-02
    • 1970-01-01
    • 2013-08-03
    • 2016-06-03
    • 1970-01-01
    • 2018-02-13
    • 2011-05-01
    • 2013-06-16
    相关资源
    最近更新 更多