python異步編程--回調模型(selectors模塊)



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:是一個實現了registerunregister的基類,注意,此基類並沒有實現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_ERADEVENT_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的類型調用對應接口,windowslinux實際調用的底層接口對比如下:

用戶統一調用高層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上,監聽的文件對象列表不可以為空:

7. 關系圖


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM