当前位置:  开发笔记 > 运维 > 正文

epoll,poll,threadpool有什么区别?

如何解决《epoll,poll,threadpool有什么区别?》经验,为你挑选了1个好方法。

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

有哪些优点/缺点?

有关框架的任何建议吗?

有关简单/基本教程的任何建议吗?

看来,epollpoll有特定的Linux ...是否有适用于Windows的等量替代?

Damon.. 211

Threadpool并不真正适合与poll和epoll相同的类别,因此我假设您将"线程池"称为"线程池以处理每个连接一个线程的多个连接".

利弊

线程池

对于中小并发而言合理有效,甚至可以胜过其他技术.

使用多个核心.

尽管某些系统(例如Linux)原则上可以安排100,000个线程,但是不能超过"几百个".

天真的实施表现出" 雷鸣般的群体 "问题.

除了上下文切换和雷鸣般的群体,人们必须考虑记忆.每个线程都有一个堆栈(通常至少为1兆字节).因此,一千个线程只需要一个GB的RAM用于堆栈.即使没有提交该内存,它仍会在32位操作系统下占用相当大的地址空间(在64位下不是真正的问题).

线程实际上可以使用epoll,虽然显而易见的方式(所有线程阻塞epoll_wait)没有用,因为epoll会唤醒每个等待它的线程,所以它仍然会有相同的问题.

最佳解决方案:单线程监听epoll,进行输入多路复用,并完成对线程池的请求.

futex是你的朋友,结合每个线程的快进队列.虽然记录严密且难以处理,但仍能futex提供所需的信息.epoll可以一次返回多个事件,并且futex允许您有效且以精确控制的方式一次唤醒N个被阻塞的线程(min(num_cpu, num_events)理想情况下为N ),并且在最好的情况下,它根本不涉及额外的系统调用/上下文切换.

实现起来并不容易,需要一些小心.

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%!).不幸的是,一个recvmmsginvokation 上的所有操作都引用相同的套接字,因此它对于基于UDP的服务非常有用(对于TCP,必须有一种recvmmsmsg系统调用,每个项目也需要一个套接字描述符!).

始终将描述符设置为非阻塞,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.

传闻更快(我从未使用它,所以无法判断这是否真实).

注册事件并在单个系统调用中返回结果集.

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 [EAGAIN EWOULDBLOCK]对于套接字,因为哦快乐,标准允许两个不同的错误值]).



1> Damon..:

Threadpool并不真正适合与poll和epoll相同的类别,因此我假设您将"线程池"称为"线程池以处理每个连接一个线程的多个连接".

利弊

线程池

对于中小并发而言合理有效,甚至可以胜过其他技术.

使用多个核心.

尽管某些系统(例如Linux)原则上可以安排100,000个线程,但是不能超过"几百个".

天真的实施表现出" 雷鸣般的群体 "问题.

除了上下文切换和雷鸣般的群体,人们必须考虑记忆.每个线程都有一个堆栈(通常至少为1兆字节).因此,一千个线程只需要一个GB的RAM用于堆栈.即使没有提交该内存,它仍会在32位操作系统下占用相当大的地址空间(在64位下不是真正的问题).

线程实际上可以使用epoll,虽然显而易见的方式(所有线程阻塞epoll_wait)没有用,因为epoll会唤醒每个等待它的线程,所以它仍然会有相同的问题.

最佳解决方案:单线程监听epoll,进行输入多路复用,并完成对线程池的请求.

futex是你的朋友,结合每个线程的快进队列.虽然记录严密且难以处理,但仍能futex提供所需的信息.epoll可以一次返回多个事件,并且futex允许您有效且以精确控制的方式一次唤醒N个被阻塞的线程(min(num_cpu, num_events)理想情况下为N ),并且在最好的情况下,它根本不涉及额外的系统调用/上下文切换.

实现起来并不容易,需要一些小心.

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%!).不幸的是,一个recvmmsginvokation 上的所有操作都引用相同的套接字,因此它对于基于UDP的服务非常有用(对于TCP,必须有一种recvmmsmsg系统调用,每个项目也需要一个套接字描述符!).

始终将描述符设置为非阻塞,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.

传闻更快(我从未使用它,所以无法判断这是否真实).

注册事件并在单个系统调用中返回结果集.

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 [EAGAIN EWOULDBLOCK]对于套接字,因为哦快乐,标准允许两个不同的错误值]).

推荐阅读
mylvfamily
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有