
本文解决 pyserial-asyncio 串口通信中因 input() 阻塞导致数据无法实时发送的问题,核心方案是使用 loop.run_in_executor() 将同步输入调度至线程池执行,确保事件循环持续运行。
本文解决 pyserial-asyncio 串口通信中因 `input()` 阻塞导致数据无法实时发送的问题,核心方案是使用 `loop.run_in_executor()` 将同步输入调度至线程池执行,确保事件循环持续运行。
在基于 pyserial-asyncio 构建的 UART 客户端/服务器应用中,一个常见却容易被忽视的陷阱是:直接在 async 函数中调用内置 input() 函数。虽然代码看似能运行(如提示输入),但 input() 是完全同步且阻塞式的——它会冻结当前线程、暂停整个 asyncio 事件循环,导致 reader.readline()、定时任务、心跳逻辑等所有异步操作全部停滞,直到用户按下回车。这正是你观察到“服务器延迟打印所有消息,仅在客户端退出后才集中输出”的根本原因:数据实际已写入串口缓冲区,但 writer.drain() 无法及时刷新(因事件循环被卡住),且 reader.readline() 因等待换行符而持续挂起,直到连接关闭触发缓冲区冲刷。
✅ 正确做法是将 input() 移出事件循环主线程,交由线程池异步执行。Python 3.8+ 完全支持 loop.run_in_executor(None, input)(None 表示使用默认 ThreadPoolExecutor),它返回一个可 await 的 Future,不会阻塞事件循环。
以下是修复后的客户端完整示例(适配你的原始结构,兼容 Python 3.8):
import asyncio
import serial_asyncio
import sys
async def uart_client(port, baudrate):
try:
reader, writer = await serial_asyncio.open_serial_connection(
url=port, baudrate=baudrate
)
print(f"Connected to {port} at {baudrate} bps")
# ✅ 异步读取:持续监听串口数据
async def receive_data():
while True:
try:
data = await reader.readline()
if data:
print(f"Received: {data.decode().strip()}")
except Exception as e:
print(f"Error reading data: {e}")
break
# ✅ 异步输入:用线程池解耦阻塞 I/O
async def send_data():
loop = asyncio.get_running_loop()
while True:
# ⚠️ 关键修复:在 executor 中执行 input,不阻塞 event loop
message = await loop.run_in_executor(None, lambda: input("Enter message (or 'exit' to quit): "))
if message.lower() == 'exit':
break
# 发送时添加 '\n' 以匹配 server 的 readline() 行协议
writer.write((message + '\n').encode())
await writer.drain() # 确保数据立即发出
print(f"Sent: {message}")
# 并发运行收发任务
await asyncio.gather(receive_data(), send_data())
except serial_asyncio.serial.serialutil.SerialException as e:
print(f"Error opening serial port: {e}")
finally:
if 'writer' in locals() and not writer.is_closing():
writer.close()
await writer.wait_closed()
print("Connection closed.")
if __name__ == "__main__":
# 注意:确保服务端使用 /dev/ttyAMA2,客户端使用 /dev/ttyAMA1(物理串口配对)
asyncio.run(uart_client('/dev/ttyAMA1', 9600))
? 关键注意事项与最佳实践:
-
行尾协议一致性:服务端使用
reader.readline(),因此客户端必须发送\n(或\r\n)作为行结束符,否则readline()将无限等待。你的原始代码已包含+ '\n',这是正确的。 -
资源清理:
writer.close()后务必await writer.wait_closed(),避免连接未真正释放。 -
串口设备权限:在 Raspberry Pi 上,确保当前用户属于
dialout组:sudo usermod -a -G dialout $USER,并重启终端。 -
调试技巧:若仍无响应,先用
echo "test" > /dev/ttyAMA1验证硬件连通性;再用stty -F /dev/ttyAMA1 9600检查波特率是否一致。 -
替代方案:若需更复杂的交互(如实时按键响应),可考虑
keyboard或prompt_toolkit库,但run_in_executor是最轻量、最符合 asyncio 哲学的标准解法。
通过将阻塞的 input() 卸载至线程池,你的串口客户端即可实现真正的“即时发送”——用户每输入一行并回车,数据即刻经 writer.write() 和 drain() 刷入串口,服务端 data_received() 几乎实时触发,彻底解决延迟累积问题。










