Python 中 IO多路复用

IO多路复用

  • 大多数操作系统都支持selectpoll
  • Linux 2.5+ 支持epoll
  • BSD、Mac支持kqueue
  • Solaris实现了/dev/poll
  • Windows的IOCP

Python的select库实现了selectpoll系统调用,这个基本上操作系统都支持。部分实现了epoll。它是底层的IO多路复用模块

开发中的选择

  1. 完全跨平台,使用selectpoll。但是性能较差
  2. 针对不同操作系统自行选择支持的技术,这样做会提高IO处理的性能

select维护一个文件描述符数据结构,单个进程使用有上限,通常是1024,线性扫描这个数据结构。效率低
poolselect区别是内部数据结构使用链表,没有这个最大限制,但是依然是线性遍历才知道哪个设备就绪了
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个属性:

  1. fileobj 注册的文件对象
  2. fd 文件描述符
  3. events 等待上面的文件描述符的文件对象的事件
  4. 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值