epoll_ctl的多线程问题


https://www.zhihu.com/question/49741301

场景:
线程A是一个循环, 调用epoll_wait, 当有事件发生时执行对应的回调函数.
线程B不时会建立新的连接, 使用non-block的socket, connect后调用epoll_ctl将socket加入监听.

线程A和线程B操作的是同一个epoll instance, 那么是否有潜在的问题了?
根据man page对于epoll_wait的描述:

While one thread is blocked in a call to epoll_pwait(), it is
       possible for another thread to add a file descriptor to the waited-
       upon epoll instance.  If the new file descriptor becomes ready, it
       will cause the epoll_wait() call to unblock.

按照我的理解, 前面的做法不会有问题.
但是实际程序运行过程出现了这样的现象: A线程正好从某次epoll_wait调用退出的时候, B线程加入的那个socket上发生的事件消失了(对应epoll_ctl返回值是0, 没有显示错误).
Google后得到的信息都是认为前述写法不存在问题, 但是偶然在一个github的项目的issue看到不一样的说法: epoll_wait() is oblivious to concurrent updates via epoll_ctl() · Issue #331 · cloudius-systems/osv · GitHub,

所以将原来的写法改了, B线程不能直接调用epoll_ctl, 而是写一个pipe唤醒A线程, 在A线程执行对应的操作. 改了之后bug没再出现了.
所以, 是man page的说法有误还是我理解有误??

 

陈硕:

用 pipe 或 eventfd 是常规的做法,我见过的网络库都这么做。

=============

https://blog.csdn.net/cws1214/article/details/47909323

 

自己以前做一个接口服务器时候,这种场景下我的设计是多个线程操作同一个epoll fd。彼时,我的理由是epoll的系列函数是线程安全的。

       当然有人不理解为什么会有多个线程操作同一个epoll fd的情形,这里稍微铺陈一下接口服务器的场景。epoll fd有线程1维护,监听服务端端口的socket的accept出来的acceptor(即新的socket fd)也放在这个epoll fd中。当收到客户端链接请求时候,线程2从连接池connector pool中挑选出来一个connector,connector的作用是转发请求,此时connector会把acceptor缓存起来。如果connector收到回复后,connector会通过acceptor向客户端返回一些数据后,线程2此时需要把acceptor在add进epoll fd中。

      以前我以为epoll fd是多线程安全的,我就直接通过epoll_ctl(epoll fd,acceptor,add)把acceptor放进epoll fd中。

      现在再回首看看,自己是想当然的这样操作了,没有任何依据。孟子曰,“行有不得,反求诸己”。既然自己无法解开困惑,那就求助伟大的man了。通过“man epoll_wait”后,得到这么一句话:

 
  1. NOTES  
  2.      While one thread is blocked in a call to epoll_pwait(), it is possible for another thread to add a file descriptor to the waited-upon epoll instance.  If the new file descriptor becomes ready, it will cause the epoll_wait() call to unblock.  
  3.      For a discussion of what may happen if a file descriptor in an epoll instance being monitored by epoll_wait() is closed in another thread, see select(2).  

 

       翻译后就是:如果一个线程正阻塞在epoll_pwait上,此时可能有另外一个线程要把一个socket fd添加到这个epoll fd上,如果这个这个新的socket fd被添加进去后处于ready状态,那么epoll_wait就不会再处于阻塞状态。如果由epoll fd监控的一个socket fd被另外一个线程close掉,此时系统处于何种状态请参考select(2)。通过"man 2 select"后,得到如下一段话:   

 
  1. Multithreaded applications  
  2.     If a file descriptor being monitored by select() is closed in another thread, the result is unspecified.  On some UNIX systems, select() unblocks and returns, with an indication that the file descriptor is ready (a subsequent I/O operation will likely fail with an error, unless another the file descriptor reopened between the time select() returned  and the I/O operations was performed).  On Linux (and some other systems), closing the file descriptor in another thread has no effect on select().  In summary, any application that relies on a particular behavior in this scenario must be considered buggy.  

 

       翻译后,其意义为:如果一个线程中由select管理的socket被另外一个线程close掉,将会发生什么只有天晓得。在一些UNIX系统中,select会结束阻塞态并返回,它会标识这个socket处于ready状态(后面对这个socket的操作会失败,os也会给出错误提示,除非在select返回和进程对这个socket进行读写这段时间段内,os又把同一个socket fd分配出去了)。在linux(和其他同类的系统)上,这种行为不会影响select(即有阻塞态变为非阻塞态)。总之,如果一个程序中这种行为应该被认为是一个bug(就不应有这种行为操作)。

       通过以上两段man大神的神示,除了一个线程在epoll或者select中监控一个socket时候另外一个线程对这个socket进行close这种情况,我就可以认为多个线程操作同一个epoll fd的行为是安全的,即我上面的操作是没有问题的。

 

==================

https://blog.csdn.net/moyuer91/article/details/51187338

https://www.zhihu.com/question/39752285/answer/82906915

转自:http://www.zhihu.com/question/39752285/answer/82906915
linux多线程网络编程中有一段话:
当然,pipe也有一个经典应用场景,那就是写Reactor/event loop 时用来唤醒异步select调用?网上没找到具体的应用场景,不知道是怎么唤醒异步调用

 

请搜索:self pipe trick。
reactor展开了写就是个等待和分发事件的过程:

events = selector.wait(milliseconds)
for fd, event in events:
    if event & EVT_READ:
       handle_read(fd)
    if event & EVT_WRITE:
       handle_write(fd)


问题出在wait上,没新消息,它将一直等到milliseconds指定的时间为准,而此时ui上用户说了句话,点击“发送”,ui线程把待发送的内容推到了网络线程的消息队列里,而网络线程还在wait呢,没网络事件的话,只有等待这轮wait结束网络线程才有空到队列里监测并处理刚才ui线程投递过来的待发送消息。

select等待时间过长将会让消息不能即时被处理,而过短又会占用过多cpu费电,因此在想能不能平时wait长一点,而当我ui线程刚点击了发送按钮就立即把网络线程从select的wait中唤醒让网络线程可以即时的查看自己的消息队列就方便了。

于是大家把管道的读取端fd放入selector,那么在wait的时候这个读取端管道fd也会一起参与wait,那么ui线程往队列里塞完任务后,马上往管道的写端写入一个字节,就可以把网络线程唤醒了。

这个方法是用来解决多个reactor之间互相唤醒的问题的,利用该技巧可以让网络线程即时处理网络事件的同时也能即时处理来自非网络(比如内部消息队列)的其它消息。

就是所谓的self pipe trick,说白了也很简单,windows下select只能针对socket套接字,不能针对管道一般用构造两个互相链接于localhost的socket来模拟之。不过win下select最多支持同时wait 64个套接字,你摸拟的pipe占掉一个,就只剩下63个可用了。

所以java的nio里selector在windows下最多支持62个套接字就是被self pipe trick占掉了两个,一个用于其它线程调用notify唤醒,另一个留作jre内部保留,就是这个原因。

说白了这其实就是个年代久远的系统层api设计考虑不周全,要应用层来给它打补丁的典型例子。

倘若系统层直接支持这样的唤醒,就不用应用层构造什么管道了。


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM