但凡写过几个网络程序的人迟早会遇到selectors这个模块。它藏在Python标准库的角落里不像requests或者flask那么出名但如果你打算从零开始写一个高性能的网络服务它就变得很重要了。说起来selectors解决的事情很简单你有一堆网络连接可能是服务器监听的那个socket也可能是和客户端通信的那些socket你需要知道哪些连接现在“有事可做”——比如有数据可以读了或者可以写数据了或者出了异常。一个很自然的想法是逐个去检查但这些socket默认是阻塞的你去读一个没有数据的socket程序就会卡在那里等。换非阻塞模式的话又要不停地轮询CPU白白烧掉了。selectors就是那个告诉你哪些socket准备好了的工具让你不用操心底层到底用的是select、poll还是epoll。你可以把它想象成一个接线员你扔给它一堆电话号码socket它帮你盯着哪个电话响铃了它马上告诉你。用法上它极简得有点让人不适应。先创建一个selector对象你可以认为是雇了一个接线员。然后用register方法把socket注册进去要告诉它你关心什么事件——是可读还是可写。最后在一个循环里不停地调用select方法它会返回一个列表里面装着已经准备好的socket和对应的事件。一个典型的服务器长这样importselectorsimportsocket selselectors.DefaultSelector()defaccept(sock,mask):conn,addrsock.accept()conn.setblocking(False)sel.register(conn,selectors.EVENT_READ,read)defread(conn,mask):dataconn.recv(1024)ifdata:print(data)else:sel.unregister(conn)conn.close()serversocket.socket()server.bind((localhost,1234))server.listen(100)server.setblocking(False)sel.register(server,selectors.EVENT_READ,accept)whileTrue:eventssel.select()forkey,maskinevents:callbackkey.data callback(key.fileobj,mask)这段代码看起来像事件驱动编程的入门模板但要注意一个细节key.data传进去的是我们注册时给的第三个参数这里传的是一个函数引用。当事件发生时selectors不会自动调用什么只是把这个函数原封不动地还给你。调用或者不调用完全由你决定。这种设计很Python——给你数据你自己决定怎么用。最佳实践方面几条经验值得留意。第一永远用DefaultSelector。它在不同操作系统上自动选择最高效的轮询机制——Linux上用epollmacOS上用kqueueWindows上用select。手动指定Selector可能会写死你的代码比如假设epoll能用。第二处理完事件后记得在合适时机注销。如果某个连接关闭而你还留着注册select会一直返回这个socket造成忙循环。第三不要在回调里做耗时操作。selectors本质上是单线程轮询如果你的回调函数里做一个数据库查询耗时两秒那么这期间其他所有连接都得不到处理。要么异步要么把任务交给线程池。和同类技术对比自然避不开asyncio。实际上Python官方的selectors文档就写着“基于select模块的高级I/O复用封装”。asyncio在底层也是用selectors来实现的只不过它包装了一层协程语法让人写起来更像同步代码。如果你的代码规模不大或者自己对事件循环控制欲强直接拿selectors做更清爽。asyncio就像帮你订好了航班、酒店、行程的旅行团选selectors是告诉你地图和交通工具在哪你自己规划路线。还有一个值得提但容易被忽略的细节selectors.select(timeoutNone)会阻塞直到有事件发生但如果传timeout0它就是一次非阻塞的查询——看看有哪些socket准备好了然后立即返回。这个特性在需要和其他非网络任务混跑的场景里很有用比如你要在同一个线程里同时处理GUI事件和网络事件可以设定一个很小的超时时间交替检查两边的队列。用selectors写出来的代码看起来有点像一种简陋的“操作系统模拟”——你自己维护着回调函数的注册表自己驱动事件循环。这种原始感其实挺有魅力的能让你感觉到网络事件驱动到底是怎么一回事。很多框架把这部分藏得太好等到出了问题需要调试时你会发现那些框架里面跑的还是同一套东西。