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
:是一个实现了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
上,监听的文件对象列表不可以为空:
7. 关系图
python异步编程--回调模型(selectors模块)的更多相关文章
- (转)python异步编程--回调模型(selectors模块)
原文:https://www.cnblogs.com/zzzlw/p/9384308.html#top 目录 0. 参考地址 1. 前言 2. 核心类 3. SelectSelector核心函数代码分 ...
- 深入理解 Python 异步编程(上)
http://python.jobbole.com/88291/ 前言 很多朋友对异步编程都处于"听说很强大"的认知状态.鲜有在生产项目中使用它.而使用它的同学,则大多数都停留在知 ...
- 深入理解Python异步编程(上)
本文代码整理自:深入理解Python异步编程(上) 参考:A Web Crawler With asyncio Coroutines 一.同步阻塞方式 import socket def blocki ...
- python 异步编程
Python 3.5 协程究竟是个啥 Yushneng · Mar 10th, 2016 原文链接 : How the heck does async/await work in Python 3.5 ...
- 这篇文章讲得精彩-深入理解 Python 异步编程(上)!
可惜,二和三现在还没有出来~ ~~~~~~~~~~~~~~~~~~~~~~~~~ http://python.jobbole.com/88291/ ~~~~~~~~~~~~~~~~~~~~~~~~~~ ...
- Python 异步编程笔记:asyncio
个人笔记,不保证正确. 虽然说看到很多人不看好 asyncio,但是这个东西还是必须学的.. 基于协程的异步,在很多语言中都有,学会了 Python 的,就一通百通. 一.生成器 generator ...
- python 并发编程 io模型 目录
python 并发编程 IO模型介绍 python 并发编程 socket 服务端 客户端 阻塞io行为 python 并发编程 阻塞IO模型 python 并发编程 非阻塞IO模型 python 并 ...
- 最新Python异步编程详解
我们都知道对于I/O相关的程序来说,异步编程可以大幅度的提高系统的吞吐量,因为在某个I/O操作的读写过程中,系统可以先去处理其它的操作(通常是其它的I/O操作),那么Python中是如何实现异步编程的 ...
- Python并发编程-事件驱动模型
一.事件驱动模型介绍 ...
随机推荐
- poj3273(二分)
题目链接:https://vjudge.net/problem/POJ-3273 题意:给定n个数,将这n个数划分成m块,问所有块最大值的最小是多少. 思路:注意到所求值最大为109,所以可以用二分来 ...
- C++中的覆盖与隐藏(详细讲解)
C++类中覆盖与隐藏一直是一个容易理解出错的地方,接下来我就详细讲解一下区别在何处 覆盖指的是子类覆盖父类函数(被覆盖),特征是: 1.分别位于子类和父类中 2.函数名字与参数都相同 3.父类的函数是 ...
- mybatis动态排序
如果我们要传入排序字段作为一个参数到mybatis中,用以实现按照指定字段来排序的功能,那么我们需要使用$,而不是像其他参数一样,使用#.如下所示. <if test="sortnam ...
- Shell脚本中"command not found"报错处理
字符串的定义与赋值 # 定义STR1变量,值为abc STR1 = "abc"(错误写法) STR1="abc"(正确写法) 在编写java代码时会考虑到格式化 ...
- easyui Tree树形控件的异步加载
Tree控件 $('#partyOrgTree').tree({ checkbox: false, url: getDataUrl, onClick: function (node) { getDiv ...
- windows下Anaconda的安装与配置正解
一.下载anaconda 第一步当然是下载anaconda了,官方网站的下载需要用迅雷才能快点,或者直接到清华大学镜像站下载. 清华大学提供了镜像,从这个镜像下载速度很快,地址: https://mi ...
- [Django] Window上通过IIS发布Django网站
网上的教程坑实在多,以下是本人亲测通过的: 需要解决的问题: 1.使用 python manage.py runserver 来运行服务器,只适用测试环境中使用,正式发布的服务,需要一个可以稳定而持续 ...
- 注解@ResponseBody的作用
@ResponseBody通常是放在方法上,主要是在前端页面异步请求的时候,返回数据使用.直白点说就是加上这个注解之后,return的数据不会解析成返回跳转路径,而是会默认放在 response b ...
- Monotonic Array LT896
An array is monotonic if it is either monotone increasing or monotone decreasing. An array A is mono ...
- RNQOJ 4 数列
把N化成二进制是关键,比如把序号10化成二进制就是1010,对于K=2来说第10个数就是2^3+2^1,对于k=3来说第10个数就是3^3+3^1;这里只需要把K替代一下就可以解决了 #include ...