Python selectors 更新附带数据:modify 已经完成,旧 SelectorKey 为什么仍显示旧阶段

38分钟前 2阅读

一个连接先处于握手阶段,收到信息后切换到正文阶段。程序已经调用selector.modify更新会话数据,调试器里早先保存的key却还写着旧阶段,于是很容易怀疑监听器没有更新。问题可能只是把一份旧注册描述当成了会自动刷新的窗口。应当分别查看旧键、当前注册表,以及真正由select返回的事件键。

本文在Linux、Python 3.12.14中使用固定样本实跑。官方文档核验于2026年10月3日;在线文档显示的补丁版本可能不同于这里记录的实际解释器。程序不访问真实业务数据,也不连接远程服务。

Python selectors 更新附带数据:modify 已经完成,旧 SelectorKey 为什么仍显示旧阶段

AI模型生成的原创概念图:同一对本地连接旁保留旧注册卡片和当前注册卡片,就绪信号指向当前信息;用于解释程序模型,不是软件截图、测试截图或实拍照片,精确行为以程序和实际输出为准。

完整程序与实际输出

将下面程序保存为tech10.py,执行python tech10.py。预期出现的异常已在例子里处理;退出码为零才表示本次演示正常完成。

import selectors
import socket

with selectors.DefaultSelector() as selector:
    left, right = socket.socketpair()
    try:
        left.setblocking(False)
        old = selector.register(left, selectors.EVENT_READ, {"phase": "old"})
        try:
            current = selector.modify(left, selectors.EVENT_READ, {"phase": "new"})
            print("same_fd=", old.fd == current.fd)
            print("old_phase=", old.data["phase"])
            print("current_phase=", selector.get_key(left).data["phase"])
            right.sendall(b"X")
            ready = selector.select(timeout=1)
            assert len(ready) == 1
            key, mask = ready[0]
            print("event_phase=", key.data["phase"])
            print("readable=", bool(mask & selectors.EVENT_READ))
            print("payload=", key.fileobj.recv(8).decode("ascii"))
        finally:
            selector.unregister(left)
        print("registered=", len(selector.get_map()))
    finally:
        left.close()
        right.close()

本次实际标准输出:

same_fd= True
old_phase= old
current_phase= new
event_phase= new
readable= True
payload= X
registered= 0

先比较三个不同的观察位置

例子只建立一对匿名本地套接字,不监听端口,也不与外部机器通信。register返回的键放进old,modify返回的键放进current。输出显示same_fd为真,old_phase还是old,而get_key查到new。这组结果说明连接没有换掉,更新的是与同一文件对象关联的注册信息。

SelectorKey是包含fileobj、fd、events和data的命名元组。modify返回新的键,早先保存的old不会被自动重写。调试时如果一直打印old.data,就等于一直看旧描述;要检查当前配置,可以使用get_key,事件处理时则优先读取这次事件携带的key.data。不要把历史日志里的对象引用误认为实时配置入口。

data替换和data内部修改不是一回事

本例给modify传入一个新的字典,因此old.data与current.data指向不同对象。旧键不是深拷贝快照:如果保存键时关联的是一个可变字典,后来直接修改那个同一字典的内容,旧键也能通过原引用看到变化。可见“键本身不可变”和“键里指向的对象不可变”是两条不同规则。

可以为会话状态选择一种一致策略。要保留阶段历史,就创建新状态对象并更新关联;要让多个处理器共享当前状态,就明确共享同一个状态容器,并管理其写入责任。混用两种策略会造成一部分日志看旧对象、一部分回调看新对象,排查时仿佛出现随机更新,实际是引用关系没有讲清楚。

就绪通知只负责告诉你可以尝试读取

右端发送一个固定字节X后,select返回的事件键带有new阶段,事件掩码也包含EVENT_READ。程序随后从key.fileobj读取这个字节,验证事件对象、关联数据和实际流端点相互一致。这里设置了一秒等待上限,测试没有靠永久阻塞等待偶然事件,也没有用休眠猜测操作系统调度顺序。

在真实事件循环中,读取就绪不等于已经收到一个完整业务消息。非阻塞读取可能获得部分内容,连接关闭也要单独处理;请求边界由协议负责。data适合保存解析缓冲和阶段等上下文,但不能把一次就绪事件直接当成一笔完整交易完成。本例固定一个字节只是为了隔离注册更新机制,并非网络协议框架。

关闭连接前先注销注册项

示例把unregister放在内部finally,再在外层finally关闭两端套接字,selector本身由with释放。输出registered=0验证正常路径上注册表已经清空。这个顺序有助于避免关闭后仍留着过期注册信息;文件描述符整数将来可能被其他资源复用,不能把它当成永久不变的业务身份。

回归测试可进一步覆盖未注册对象调用modify、重复register、数据更新后立即查询、读取后无新数据等情况。并发框架中还要明确哪个线程管理注册表,不能因为data可以放任意对象,就假定跨线程修改自动得到同步。最关键的验收标准是当前事件能找到正确会话,而不是某个旧变量在调试器里恰好显示什么。

验证记录与参考资料

本次完整程序退出码为0,标准错误为空。本例验证本地套接字的注册数据更新与正常注销;不替代完整协议、连接异常或多线程访问测试。


文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。