可能有人解释的区别是什么之间epoll
,poll
和线程池?
有哪些优点/缺点?
有关框架的任何建议吗?
有关简单/基本教程的任何建议吗?
看来,epoll
和poll
有特定的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)
非常适合timerfd
和eventfd
(令人惊叹的计时器分辨率和准确性).
与之相得益彰signalfd
,消除了对信号的笨拙处理,使它们以非常优雅的方式成为正常控制流程的一部分.
epoll实例可以递归地托管其他epoll实例
该编程模型所做的假设:
大多数描述符大部分时间处于空闲状态,很少有东西(例如"接收数据","连接关闭")实际上在少数描述符上发生.
大多数情况下,您不希望从集合中添加/删除描述符.
大多数时候,你在等待一些事情发生.
一些小的陷阱:
一个级别触发的epoll唤醒所有等待它的线程(这是"按预期工作"),因此将epoll与线程池一起使用的天真方式是无用的.至少对于TCP服务器来说,这不是什么大问题,因为无论如何都必须首先组装部分请求,所以一个天真的多线程实现不会做任何一种方式.
不能像文件读/写那样工作("总是准备好").
直到最近才能与AIO一起使用,现在可以通过eventfd
,但需要(迄今为止)未记录的功能.
如果上述假设不成立,则epoll可能效率低下,并且poll
可能表现相同或更好.
epoll
不能做"魔术",即就发生的事件数量而言,它仍然必然是O(N).
但是,它epoll
可以很好地处理新的recvmmsg
系统调用,因为它一次返回几个就绪通知(尽可能多,直到你指定的任何一个maxevents
).这样就可以在繁忙的服务器上通过一个系统调用接收例如15个EPOLLIN通知,并使用第二个系统调用读取相应的15个消息(系统调用减少93%!).不幸的是,一个recvmmsg
invokation 上的所有操作都引用相同的套接字,因此它对于基于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]对于套接字,因为哦快乐,标准允许两个不同的错误值]).
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)
非常适合timerfd
和eventfd
(令人惊叹的计时器分辨率和准确性).
与之相得益彰signalfd
,消除了对信号的笨拙处理,使它们以非常优雅的方式成为正常控制流程的一部分.
epoll实例可以递归地托管其他epoll实例
该编程模型所做的假设:
大多数描述符大部分时间处于空闲状态,很少有东西(例如"接收数据","连接关闭")实际上在少数描述符上发生.
大多数情况下,您不希望从集合中添加/删除描述符.
大多数时候,你在等待一些事情发生.
一些小的陷阱:
一个级别触发的epoll唤醒所有等待它的线程(这是"按预期工作"),因此将epoll与线程池一起使用的天真方式是无用的.至少对于TCP服务器来说,这不是什么大问题,因为无论如何都必须首先组装部分请求,所以一个天真的多线程实现不会做任何一种方式.
不能像文件读/写那样工作("总是准备好").
直到最近才能与AIO一起使用,现在可以通过eventfd
,但需要(迄今为止)未记录的功能.
如果上述假设不成立,则epoll可能效率低下,并且poll
可能表现相同或更好.
epoll
不能做"魔术",即就发生的事件数量而言,它仍然必然是O(N).
但是,它epoll
可以很好地处理新的recvmmsg
系统调用,因为它一次返回几个就绪通知(尽可能多,直到你指定的任何一个maxevents
).这样就可以在繁忙的服务器上通过一个系统调用接收例如15个EPOLLIN通知,并使用第二个系统调用读取相应的15个消息(系统调用减少93%!).不幸的是,一个recvmmsg
invokation 上的所有操作都引用相同的套接字,因此它对于基于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]对于套接字,因为哦快乐,标准允许两个不同的错误值]).