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”后,得到這么一句話:
- NOTES
- 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.
- 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"后,得到如下一段話:
- Multithreaded applications
- 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
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設計考慮不周全,要應用層來給它打補丁的典型例子。
倘若系統層直接支持這樣的喚醒,就不用應用層構造什么管道了。