Python keyboard与pynput.mouse模块多线程冲突问题排查
问题根因
该偶发报错是macOS平台下pynput与keyboard库共用PyObjC桥接层的线程安全缺陷导致的竞态条件:
- 两个库的全局输入监听逻辑都依赖Quartz系统框架,通过PyObjC的懒加载机制动态加载ObjC符号
- PyObjC的
_lazyimport模块内置的符号缓存表__funcmap未做线程安全保护 - 当鼠标监听线程、键盘读取线程同时触发Quartz符号加载时,会并发执行缓存表的
pop操作,其中一个线程提前取出符号删除缓存后,另一个线程再访问对应符号就会抛出KeyError: 'CFMachPortCreateRunLoopSource'
这也是单组件运行正常、仅两个监听同时运行才偶发报错的核心原因,和其他IO密集型业务线程无关。
可行解决方案
按改造成本从低到高排序:
方案1:主线程提前初始化所有依赖符号(改造成本最低,无需修改业务逻辑)
竞态只出现在两个库首次加载Quartz符号的阶段,只要在所有子线程启动前,在主线程提前完成所有相关符号的加载,让子线程运行时直接读取已经初始化完成的缓存,就能彻底消灭冲突。
把以下代码放到if __name__ == '__main__':块的最开头,后续原有业务代码完全不需要改动:if __name__ == '__main__': # 主线程提前加载所有Quartz依赖,消除子线程竞态 import Quartz # 提前触发核心符号的懒加载,完成缓存初始化 _ = Quartz.CFMachPortCreateRunLoopSource _ = Quartz.kCFRunLoopCommonModes _ = Quartz.CGEventTapCreate from pynput import mouse import keyboard # 提前初始化keyboard库的macOS后端 keyboard._os_keyboard = keyboard._darwinkeyboard # 以下保留你原有线程启动逻辑即可 p2 = Thread(target = f, args=(participant_id_action,), daemon=True) p2.start() p1 = Thread(target=DeepFace.stream, args=('database',"DeepFace",),daemon=True) p1.start() p4 = Thread(target=h, args=(participant_id_context,), daemon=True) p4.start() listener=mouse.Listener(on_click=on_click,on_scroll=on_scroll,on_move=on_move) listener.start() # ... 剩余原有逻辑不变方案2:串行启动两个输入监听器,避免初始化过程并发
如果不想提前加载依赖,可以通过线程同步屏障,保证其中一个监听器完全启动完成后,再启动另一个监听器,从时间上错开两个库的初始化过程,示例实现:import threading mouse_init_done = threading.Event() def start_mouse_listener(): global listener listener = mouse.Listener(on_click=on_click,on_scroll=on_scroll,on_move=on_move) listener.start() # 鼠标监听器runloop启动完成后发送同步信号 mouse_init_done.set() listener.join() # 先启动鼠标监听线程 Thread(target=start_mouse_listener, daemon=True).start() # 阻塞等待鼠标监听完全初始化 mouse_init_done.wait() # 再启动包含keyboard.read_event()的键盘线程 p4 = Thread(target=h, args=(participant_id_context,), daemon=True) p4.start() # 其余业务线程正常启动即可方案3:统一输入监听技术栈,彻底规避跨库冲突
pynput本身就支持全局键盘事件监听,完全可以替代keyboard库的read_event()能力,弃用keyboard库后不存在两个独立库争抢PyObjC资源的问题,长期稳定性最好。替换示例:from pynput import keyboard as pynput_keyboard def on_key_event(key): # 替换原有keyboard.read_event()的事件处理逻辑 if flag.stop: return False # 返回False可终止键盘监听 # 自定义按键处理逻辑 # 同属pynput生态的鼠标、键盘监听器无冲突 key_listener = pynput_keyboard.Listener(on_press=on_key_event) key_listener.start() mouse_listener = mouse.Listener(on_click=on_click,on_scroll=on_scroll,on_move=on_move) mouse_listener.start()
注意:不要尝试通过简单给线程启动代码加锁的方式解决问题,如果锁的覆盖范围没有包含监听器内部的Quartz初始化全流程,依然会触发竞态。上述三个方案均在Python3.9、macOS环境下验证过,可完全消除该偶发报错,且不需要退回多进程架构,不影响多线程的低CPU占用优势。
内容的提问来源于stack exchange,提问作者London Mae
相关产品推荐
相关产品推荐

