
本文详解 keyboard 模块在多线程(尤其配合 asyncio)环境下调用 is_pressed()、read_event() 等函数时无响应、卡死的根本原因,并提供稳定替代方案——推荐使用 Windows 原生 msvcrt 模块实现非阻塞、可中断、线程安全的键盘输入检测。
本文详解 `keyboard` 模块在多线程(尤其配合 `asyncio`)环境下调用 `is_pressed()`、`read_event()` 等函数时无响应、卡死的根本原因,并提供稳定替代方案——推荐使用 windows 原生 `msvcrt` 模块实现非阻塞、可中断、线程安全的键盘输入检测。
在 Python GUI 或后台服务类脚本中,常需同时运行异步任务(如网络连接)与实时键盘交互(如快捷键控制)。但许多开发者发现:一旦启动独立线程执行 asyncio.run(),原本正常工作的 keyboard.is_pressed() 突然“冻结”——既不返回 True/False,也无法被 Ctrl+C 中断,整个主线程陷入假死。根本原因在于:keyboard 模块底层依赖全局钩子(Windows 下为 SetWindowsHookEx)和专用监听线程,其事件循环与 asyncio 主循环或 threading 的调度存在资源竞争与消息泵冲突;更关键的是,keyboard 的 is_pressed() 是伪轮询——它内部会尝试同步读取钩子队列,但在多线程+异步混合场景下极易因锁竞争或消息丢失而永久阻塞。
值得注意的是,keyboard.add_hotkey() 虽能绕过此问题(因其注册回调,不主动轮询),但它无法清空系统键盘缓冲区,导致按键残留——当程序退出或后续调用 input() 时,积压的按键会突然“爆发式”输入,破坏交互逻辑。同样,pynput 也存在缓冲区不可控问题。
✅ 推荐方案:使用 msvcrt(仅限 Windows)实现轻量、可靠、无缓冲污染的键盘检测
msvcrt 是 Python 标准库中专为 Windows 设计的底层 C 运行时接口,其 kbhit() 和 getch() 行为与 C 语言完全一致:
- msvcrt.kbhit():立即返回布尔值,绝不阻塞,线程安全;
- msvcrt.getch():仅在有按键时读取单字节(含功能键转义序列),自动消耗输入,不留缓冲;
- 完美兼容多线程与 asyncio —— 因其不依赖全局钩子,而是直接查询控制台输入缓冲区。
以下为修复后的完整示例(已移除 keyboard 依赖,支持多线程 + 键盘实时响应):
立即学习“Python免费学习笔记(深入)”;
import threading
import asyncio
import time
import msvcrt # ✅ 替代 keyboard,仅 Windows 有效
flagkeypress = False
keycode = 'a'
flagserverconnected = False
def task1():
print("Connecting to server")
asyncio.run(connect2svr())
print("task1 ended")
async def connect2svr():
global flagkeypress, keycode, flagserverconnected
try:
print("Connected to server")
flagserverconnected = True
keeploopBLE = True
while keeploopBLE:
if flagkeypress:
flagkeypress = False
if keycode == 'q':
for k in range(14, 19):
sk = "##SP" + f"{k:02}"
print(sk)
await asyncio.sleep(1)
elif keycode == 't': # 映射 't' 为退出
keeploopBLE = False
except Exception as e:
print(f"Error: {e}")
# === 主程序:使用 msvcrt 替代 keyboard ===
print("Start of my main program")
t1 = threading.Thread(target=task1, daemon=True) # 设为守护线程,避免阻塞退出
t1.start()
keeploop = True
while keeploop:
print("\nOptions: [Q]=Get Version, [T]=Get Time, [P]=Start/End Get GPS Time\n")
print("Press key for command:")
waitkey = True
while waitkey and keeploop:
# ✅ 非阻塞检测:msvcrt.kbhit() 立即返回
if msvcrt.kbhit():
key_byte = msvcrt.getch() # ✅ 自动消费按键,无残留
try:
key_char = key_byte.decode('utf-8').lower()
except UnicodeDecodeError:
key_char = '' # 忽略非 UTF-8 字节(如方向键)
if key_char == 't':
print(f"key '{key_char}' is pressed, program going to end")
keycode = 't'
flagkeypress = True
waitkey = False
keeploop = False
elif key_char == 'q':
print(f"key '{key_char}' is pressed")
keycode = 'q'
flagkeypress = True
waitkey = False
else:
print(f"Ignored key: {repr(key_byte)}")
print("loop 1 time now")
time.sleep(0.2) # 控制轮询频率,降低 CPU 占用
time.sleep(0.5)
print("Main program ended")
# t1.join() # 守护线程无需显式 join,主程序退出时自动终止⚠️ 注意事项与最佳实践
- msvcrt 仅适用于 Windows。若需跨平台,请结合 sys.platform 分支处理,Linux/macOS 可用 select + sys.stdin 实现类似非阻塞读取;
- msvcrt.getch() 返回 bytes,需注意解码(ASCII 键盘字符可直接 decode,特殊键如 F1 会返回 \x00 或 \xe0 开头的双字节序列,需额外处理);
- 避免在 asyncio 主循环中直接调用 msvcrt(因其是同步阻塞式 I/O),应始终在普通线程中使用;
- 将工作线程设为 daemon=True 可防止主线程退出时被卡住,符合脚本生命周期管理规范;
- 永远不要在多线程环境中混用 keyboard 的轮询 API(is_pressed, read_event)——这是已知的稳定性雷区。
综上,keyboard 模块的设计目标是全局热键与宏录制,而非实时交互控制;当需求聚焦于“可控、低延迟、无副作用”的终端键盘响应时,回归标准库的 msvcrt(Windows)或 POSIX 的 termios(Linux/macOS)才是健壮之选。


















