epoll_ctl的多线程问题
场景:
线程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看到不⼀样的说法: ,
所以将原来的写法改了, B线程不能直接调⽤epoll_ctl, ⽽是写⼀个pipe唤醒A线程, 在A线程执⾏对应的操作. 改了之后bug没再出现了.
所以, 是man page的说法有误还是我理解有误??
陈硕:
⽤ pipe 或 eventfd 是常规的做法,我见过的⽹络库都这么做。
=============
⾃⼰以前做⼀个接⼝服务器时候,这种场景下我的设计是多个线程操作同⼀个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
翻译后,其意义为:如果⼀个线程中由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的⾏为是安全的,即我上⾯的操作是没有问题的。
==================
转⾃:www.zhihu/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)
reactor线程模型 java问题出在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设计考虑不周全,要应⽤层来给它打补丁的典型例⼦。倘若系统层直接⽀持这样的唤醒,就不⽤应⽤层构造什么管道了。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论