0. 參考地址
基本介紹 https://www.cnblogs.com/yinheyi/p/8127871.html
實驗演示 https://www.cnblogs.com/xybaby/p/6406191.html#_label_2
詳細講解 http://aju.space/2017/07/31/Drive-into-python-asyncio-programming-part-1.html
官方文檔selecotors https://docs.python.org/3/library/selectors.html
官方文檔select https://docs.python.org/3/library/select.html
1. 前言
並發的解決方案中,因為阻塞IO調用的原因,同步模型(串行/多進程/多線程)並不適合大規模高並發.在非阻塞IO調用中,我們可以使用一個線程完成高並發的功能,不過因為非阻塞IO會立即返回,如何判斷IO准備就緒以及就緒之后如何處理就變成了關鍵,所以我們需要附帶額外的處理.
不論使用哪一種額外處理方式,核心都是為了獲知IO准備就緒且執行對應的操作,額外處理方式之一就是回調+事件循環.
OS
已經為我們提供了select/poll/epoll/kqueue
等多種底層操作系統接口用以處理IO准備就緒的通知(即通過OS
提供的接口可以方便的編寫事件循環).而程序還需要完成:如何在IO准備就緒的時候執行預定的操作.
selecotrs
模塊,總代碼611
行,其中有5
個類是同一個級別,只是根據OS
的類型而有所不同.模塊中還包含大量的注釋,所以核心代碼數量就在100
行左右.selectors
模塊為我們提供了異步編程中的回調模型(后面還會寫異步編程中的協程模型),所以我覺得對此模塊的研究是很有必要的.
2. 核心類
selectors
模塊中的核心類如下:
BaseSelector
:是一個抽象基類,定義了核心子類的函數接口.BaseSelector
類定義的核心接口如下:
@abstractmethod
register(self, fileobj, events, data=None) # 提供文件對象的注冊
@abstractmethod
unregister(self, fileobj) # 注銷已注冊的文件對象
@abstractmethod
select(self, timeout=None) # 向OS查詢准備就緒的文件對象
其中,前兩個函數封裝了文件對象,並提供了data
變量用於保存附加數據,這就提供了回調的環境.第三個函數select
是對OS
底層select/poll/epoll
接口的封裝,用以提供一個統一的對外接口.
_BaseSelectorImpl
:是一個實現了register
和unregister
的基類,注意,此基類並沒有實現select
函數,因為select
函數在不同OS上
使用的底層接口不同,所以應該在對應的子類中定義
SelectSelector
:使用windows
時的接口
EpollSelector
:使用linux
時的接口(其他3
個類相似,只是應用於不同的OS
)
DefaultSelector
:此為類別名,selectors
模塊會根據所在操作系統的類型,選擇最優的接口
如下只對selectselector
類的核心代碼進行分析,其他對應類的代碼邏輯基本一致.
3. SelectSelector核心函數代碼分析
有名元祖selectorkey
SelectorKey = namedtuple('SelectorKey', ['fileobj', 'fd', 'events', 'data'])
此對象是一個有名元祖,可以認為是對文件對象fileobj
,對應的描述符值fd
,對應的事件events
,附帶的數據data
這幾個屬性的封裝.此對象是核心操作對象,關聯了需要監控的文件對象,關聯了需要OS
關注的事件,保存了附帶數據(其實這里就放的回調函數)
3.1 注冊
def __init__(self):
super().__init__()
self._readers = set() # 使用集合處理唯一性
self._writers = set()
首先,構造函數中定義了_readers
和_writers
變量用於保存需要監聽的文件對象的文件描述符值,並使用集合特性來處理唯一性.
def register(self, fileobj, events, data=None):
key = super().register(fileobj, events, data)
if events & EVENT_READ:
self._readers.add(key.fd)
if events & EVENT_WRITE:
self._writers.add(key.fd)
return key
一般我們使用register
作為第一個操作的函數,代表着你需要監聽的文件對象,以及,當它發生你關注的事件時,你要如何處理.
此函數有3
個參數,分別是文件對象,監聽事件(可讀為1
,可寫為2
),附帶數據.
fileobj
文件對象是類文件對象,與平台強相關,在windows
上只能是socket
,在linux
上可以是任何linux
支持的文件對象.
events
是一個int
類型的值,就是EVENT_ERAD
和EVENT_WRITE
data
是附帶數據,我們可以把回調函數放在這里
此函數返回的key
就是一個selectorkey
有名元祖
register
函數將用戶監聽的文件對象和事件注冊到有名元祖中,並加入監聽集合_readers
和_writers
中
3.2 注銷
def unregister(self, fileobj):
key = super().unregister(fileobj)
self._readers.discard(key.fd)
self._writers.discard(key.fd)
return key
當我們不需要監聽某一個文件對象時,使用unregister
注銷它.這會使得它從_readers
和_writers
中被彈出.
3.3 查詢
def select(self, timeout=None):
timeout = None if timeout is None else max(timeout, 0)
ready = []
try:
r, w, _ = self._select(self._readers, self._writers, [], timeout)
except InterruptedError:
return ready
r = set(r)
w = set(w)
for fd in r | w:
events = 0
if fd in r:
events |= EVENT_READ
if fd in w:
events |= EVENT_WRITE
key = self._key_from_fd(fd)
if key:
ready.append((key, events & key.events))
return ready
這段代碼描述了用戶向OS
發起的查詢邏輯.select
函數的timeout
參數默認是None
,這意味着默認情況下,如果沒有任何一個就緒事件的發生,select
調用會被永遠阻塞.
select
函數調用底層select/poll/epoll
接口,此函數在SelectSelector
類和EpollSelector
類中的定義有所區別,會根據OS
的類型調用對應接口,windows
和linux
實際調用的底層接口對比如下:
用戶統一調用高層select函數,此函數實際調用的接口為:
# windows下使用select(SelectSelector類)
r, w, _ = self._select(self._readers, self._writers, [], timeout)
# linux下使用epoll(EpollSelector類)
fd_event_list = self._epoll.poll(timeout, max_ev)
函數使用ready
變量保存准備就緒的元祖(key, events)
在windows
中,一旦底層select
接口返回,會得到3
個列表,前兩個表示可讀和可寫的文件對象列表,並使用集合處理為唯一性.准備就緒的元祖對象會加入ready
列表中返回.如果定義了timeout
不為None
,且發生了超時,會返回一個空列表.
4. 別名
# Choose the best implementation, roughly:
# epoll|kqueue|devpoll > poll > select.
# select() also can't accept a FD > FD_SETSIZE (usually around 1024)
if 'KqueueSelector' in globals():
DefaultSelector = KqueueSelector
elif 'EpollSelector' in globals():
DefaultSelector = EpollSelector
elif 'DevpollSelector' in globals():
DefaultSelector = DevpollSelector
elif 'PollSelector' in globals():
DefaultSelector = PollSelector
else:
DefaultSelector = SelectSelector
selectors
模塊定義了一個別名DefaultSelector
用於根據OS
類型自動指向最優的接口類.
5. 總結
1 操作系統提供的select/poll/epoll
接口可以用於編寫事件循環,而selectors
模塊封裝了select
模塊,select
模塊是一個低級別的模塊,封裝了select/poll/epoll/kqueue
等接口.
2 selectors
模塊中定義了有名元祖selectorkey
,此對象封裝了文件對象/描述符值/事件/附帶數據,selectorkey
為我們提供了回調的環境
3 使用selectors
模塊可以實現使用回調模型來完成高並發的方案.
4 (非常重要)異步回調模型,大部分事件和精力都是對回調函數的設計.回調模型使得每一個涉及IO操作的地方都需要單獨分割出來作為函數,這會分割代碼導致可讀性下降和維護難度的上升.
5 回調函數之間的通信很困難,需要通過層層函數傳遞.
6 回調模型很難理解
6. 代碼報錯問題
1. 文件描述符數量
Traceback (most recent call last):
File "F:/projects/hello/hello.py", line 119, in <module>
loop()
File "F:/projects/hello/hello.py", line 102, in loop
events = selector.select()
File "F:\projects\hello\selectors.py", line 323, in select
r, w, _ = self._select(self._readers, self._writers, [], timeout)
File "F:\projects\hello\selectors.py", line 314, in _select
r, w, x = select.select(r, w, w, timeout)
ValueError: too many file descriptors in select()
在windows
上,底層使用的是select
接口,可以支持的文件描述符數量理論說是1024
,實際測試描述符必須小於512
(我的電腦是win10 64bit
)
在linux
上使用的是epoll
,可以支持大於1024
的文件描述符數量,不過測試發現在達到4000
左右的時候也會報錯。
stack overflow
解釋1:https://stackoverflow.com/questions/31321127/too-many-file-descriptors-in-select-python-in-windows
stack overflow
解釋2:
https://stackoverflow.com/questions/47675410/python-asyncio-aiohttp-valueerror-too-many-file-descriptors-in-select-on-win
2. 監聽列表是否可以為空
Traceback (most recent call last):
File "F:/projects/hello/world.py", line 407, in <module>
loop()
File "F:/projects/hello/world.py", line 378, in loop
events = selector.select()
File "F:\projects\hello\selectors.py", line 323, in select
r, w, _ = self._select(self._readers, self._writers, [], timeout)
File "F:\projects\hello\selectors.py", line 314, in _select
r, w, x = select.select(r, w, w, timeout)
OSError: [WinError 10022] 提供了一個無效的參數。
在windows
上,監聽的文件對象列表不可以為空: