Python 中 IO多路复用
IO多路复用
- 大多数操作系统都支持
select
和poll
- Linux 2.5+ 支持
epoll
- BSD、Mac支持
kqueue
- Solaris实现了
/dev/poll
- Windows的
IOCP
Python的select库
实现了select
、poll
系统调用,这个基本上操作系统都支持。部分实现了epoll
。它是底层的IO
多路复用模块
开发中的选择
- 完全跨平台,使用
select
、poll
。但是性能较差 - 针对不同操作系统自行选择支持的技术,这样做会提高IO处理的性能
select
维护一个文件描述符数据结构,单个进程使用有上限,通常是1024,线性扫描这个数据结构。效率低。
pool
和select
的区别是内部数据结构使用链表,没有这个最大限制,但是依然是线性遍历才知道哪个设备就绪了
epool
使用事件通知机制,使用回调机制提高效率。
select/pool
还要从内核空间复制消息到用户空间,而epoll
通过内核空间和用户空间共享一块内存来减少复制
selectors库
3.4版本
提供selectors库,高级IO
复用库
类层次结构︰
BaseSelector
+-- SelectSelector 实现select
+-- PollSelector 实现poll
+-- EpollSelector 实现epoll
+-- DevpollSelector 实现devpoll
+-- KqueueSelector 实现kqueue
selectors.DefaultSelector
返回当前平台最有效、性能最高的实现。
但是,由于没有实现Windows下的IOCP,所以,Windows下只能退化为select
- 在selects模块源码最下面有如下代码
# 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
- 事件注册
class SelectSelector(_BaseSelectorImpl):
"""Select-based selector."""
def register(fileobj, events, data=None) -> SelectorKey: pass
为selector
注册一个文件对象,监视它的IO
事件。返回SelectKey
对象。
fileobj
被监视文件对象,例如socket
对象events
事件,该文件对象必须等待的事件data
可选的与此文件对象相关联的不透明数据,例如,关联用来存储每个客户端的会话ID,关联方法。通过这个参数在关注的事件产生后让selector
干什么事
Event常量 | 含义 |
---|---|
EVENT_READ | 可读 0b01,内核已经准备好输入输出设备,可以开始读了 |
EVENT_WRITE | 可写 0b10,内核准备好了,可以往里写了 |
selectors.SelectorKey
有4个属性:
fileobj
注册的文件对象fd
文件描述符events
等待上面的文件描述符的文件对象的事件data
注册时关联的数据
练习:IO多路复用TCP Server
完成一个TCP Server,能够接受客户端请求并回应客户端消息
import selectors
import socket
# 构建本系统最优Selector
s = selectors.DefaultSelector() # 拿到selector
# 准备类文件对象
server = socket.socket()
server.bind(('127.0.0.1', 9999))
server.listen()
# 官方建议采用非阻塞ID
server.setblocking(False)
# 回调函数,sock的读事件
# 形参自定义
def accept(sock:socket.socket, mask:int):
"""mask:事件的掩码"""
conn, raddr = sock.accept()
conn.setblocking(False) # 非阻塞
key = s.register(conn, selectors.EVENT_READ, recv) # 读和写可同时监控, selectors.EVENT_READ | selectors.EVENT_WRITE
print(key)
# 回调函数
def recv(conn:socket.socket, mast:int):
data = conn.recv(1024)
print(data)
msg = 'Your msg = {} from {}'.format(data.decode(), conn.getpeername()).encode()
conn.send(msg)
# 注册关注的类文件对象和其事件们 'fileobj', 'fd', 'events', 'data'
# 返回SelectorKey对象
key = s.register(server, selectors.EVENT_READ, accept) #socket fileobject
print(key)
print(key.__class__.mro())
while True:
# 监听注册的对象的事件,发生被关注事件则返回events
events = s.select() # epoll select, 默认是阻塞的
# 当你注册的文件对象们, 这其中的至少一个对象关注的时间就绪了,就不阻塞了
print(events) # [(key, mask)]
for key, mask in events:
# key.data => accept; key.fileobj => sock
# 每一个event都是某一个被观察的就绪的对象
print(type(key), type(mask))
print(key.data)
key.data(key.fileobj, mask)
实战:IO多路复用群聊软件
将ChatServer改写成IO多路复用的方式
不需要启动多线程来执行socket的accept、recv方法了
import socket
import threading
import selectors
import logging
FORMAT = "%(asctime)s %(threadName)s %(thread)d %(message)s"
logging.basicConfig(format=FORMAT, level=logging.INFO)
class ChatServer:
def __init__(self, ip='127.0.0.1', port=9999):
self.sock = socket.socket()
self.addr = ip, port
self.event = threading.Event()
# 构建本系统最优Selector
self.selector = selectors.DefaultSelector()
def start(self):
self.sock.bind(self.addr)
self.sock.listen()
self.sock.setblocking(False)
# 注册sock的被关注事件,返回SelectorKey对象
# key记录了fileobj, fileobj的fd, events, data
key = self.selector.register(self.sock, selectors.EVENT_READ, self.accept)
logging.info(key) # 只有一个
# self.acceptkey = key
# select用的
threading.Thread(target=self.select, name='select', daemon=True).start()
def select(self):
while not self.event.is_set():
# 监听注册的对象的事件,发生被关注事件则返回events
events = self.selector.select() # 阻塞
print(events) # [(key, mask)]
for key, mask in events:
# key.data => accept; key.fileobj => sock
key.data(key.fileobj, mask) # select线程
# 回调函数,sock的读事件
# 形参自定义
def accept(self, sock:socket.socket, mask):
"""mask:事件的掩码"""
conn, raddr = self.sock.accept()
conn.setblocking(False) # 非阻塞
key = self.selector.register(conn, selectors.EVENT_READ, self.recv)
logging.info(key) #n个
# 回调函数
def recv(self, conn:socket.socket, mask):
data = conn.recv(1024)
if data.strip() == b'quit' or data.strip() == b'':
# bye
self.selector.unregister(conn)
conn.close()
return
for key in self.selector.get_map().values():
# if key.fileobj is self.sock :continue
# print(self.recv is key.data) # False
# print(self.recv == key.data)
if key.data == self.recv: # key.data 注册时注入的绑定的对象
s = key.fileobj
msg = 'Your msg = {} from {}'.format(data.decode(), conn.getpeername()).encode()
s.send(msg)
def stop(self):
self.event.set()
fs = []
for key in self.selector.get_map().values():
fs.append(key.fileobj)
for f in fs:
self.selector.unregister(f)
f.close()
self.selector.close()
if __name__ == '__main__':
cc = ChatServer()
cc.start()
while True:
cmd= input('>>').strip()
if cmd == 'quit':
cc.stop()
break
logging.info(threading.enumerate())
本例只完成基本功能,其他功能如有需要,请自行完成。
注意使用IO多路复用,使用了几个线程?
特别注意key.data == self.recv
总结
使用 IO多路复用 +(select、epoll)
并不一定比 多线程 + 同步阻塞IO
性能好,其最大优势可以处理更多的连接
-
多线程 + 同步阻塞IO模式
开辟太多线程,线程开辟、销毁开销还是较大,倒是可以使用线程池;线程多,线程自己使用的内存也很可观;多线程切换时要保护现场和恢复现场,线程过多,切换会占用大量的时间 -
连接较少,多线程 + 同步阻塞IO模式比较适合,效率也不低
-
如果连接非常多,对服务端程序来说,IO并发还是比较高的,这时候,开辟太多线程其实也不是很划算,这时候IO多路复用或许是更好的选择,使用
epoll