Python串口通信怎么避免阻塞?多线程与异步读取实现方案

竹里
竹里
发布时间:2026-10-26| 5人浏览过
原创

在Python开发上位机、工控网关或物联网设备交互时,串口(UART/RS-232/RS-485)读取阻塞是一个极高频出现的问题。核心解决路径非常明确:首先需合理配置底层驱动的超时参数以消除死锁挂起,随后根据系统并发模型,选择“独立接收线程配合线程安全队列”或“基于事件循环的asyncio异步协程”来将串口I/O从主逻辑中完全解耦。本文将直接展示两种主流非阻塞读取方案的具体实现、数据流向设计与适用边界。

一、串口为什么会阻塞:阻塞根源与基础超时参数

在使用 pyserial 库执行 ser.read() 或 ser.readline() 时,默认底层调用处于阻塞模式。若打开串口时未显式指定 timeout 参数(即 timeout=None),一旦对端设备未发送预期的换行符或数据字节不足,Python运行时将无限期等待操作系统内核缓冲区的数据填入,导致整个程序失去响应。

Python串口通信怎么避免阻塞?多线程与异步读取实现方案
图1:串口阻塞与非阻塞通信机制对比图

针对最轻量的避免死锁需求,可以通过 Serial(timeout=0.1) 设定读取超时(单位为秒)。当指定时间内未收到足够字节时,调用会返回当前已收到的数据切片或空字节串 b''。但单纯依靠主循环轮询 in_waiting 与 timeout 依然会消耗CPU时间片,难以支撑复杂的应用架构。

二、多线程与队列架构:经典且高兼容的解耦方案

多线程方案适用于绝大多数桌面上位机(如PyQt、Tkinter应用)或多模块同步后台服务。其基本思想是:创建一个专门的后台守护线程(Daemon Thread)负责串行总线的读操作,读出的数据通过 queue.Queue 安全地投递给主工作线程。

Python串口通信怎么避免阻塞?多线程与异步读取实现方案
图2:基于多线程与线程安全队列的架构解耦图

多线程读取的标准实现代码

import time
import serial
import threading
import queue

class ThreadedSerialWorker:
    def __init__(self, port: str, baudrate: int = 115200):
        # 设置合理的超时时间,避免关闭串口时线程永久卡死
        self.ser = serial.Serial(port=port, baudrate=baudrate, timeout=0.2)
        self.rx_queue = queue.Queue(maxsize=1000)
        self.stop_event = threading.Event()
        self.worker_thread = threading.Thread(target=self._read_loop, daemon=True)

    def start(self):
        self.worker_thread.start()

    def _read_loop(self):
        while not self.stop_event.is_set():
            try:
                # 检查输入缓冲区是否有可用数据
                if self.ser.in_waiting > 0:
                    raw_data = self.ser.readline()
                    if raw_data:
                        # 丢入阻塞队列供主线程消费
                        try:
                            self.rx_queue.put(raw_data, timeout=0.1)
                        except queue.Full:
                            # 队列满时按需丢弃或告警,防止内存膨胀
                            pass
                else:
                    # 避免在无数据时造成CPU空转
                    time.sleep(0.01)
            except (serial.SerialException, OSError) as e:
                # 捕获物理拔出或链路断开异常
                self.rx_queue.put(f"ERROR: {e}".encode())
                break

    def stop(self):
        self.stop_event.set()
        if self.worker_thread.is_alive():
            self.worker_thread.join(timeout=1.0)
        if self.ser.is_open:
            self.ser.close()

# 示例:主线程调用演示
if __name__ == "__main__":
    worker = ThreadedSerialWorker(port="/dev/ttyUSB0", baudrate=115200)
    worker.start()
    try:
        for _ in range(5):
            try:
                msg = worker.rx_queue.get(timeout=1.0)
                print(f"收到串口消息: {msg.decode(errors='ignore').strip()}")
            except queue.Empty:
                print("暂无新数据,主线程继续执行其他任务...")
            time.sleep(0.5)
    finally:
        worker.stop()
        print("通信已安全关闭。")

三、异步非阻塞方案:基于 asyncio 与 aioserial 的高并发读取

当系统整体基于现代异步网络框架(如 FastAPI、aiohttp 或纯异步守护进程)构建时,混合多线程会带来线程管理与上下文切换的开销。借助 asyncio 与封装好的 aioserial,可以将串口文件描述符直接注册至系统的事件循环(如 Linux epoll 或 Windows IOCP),彻底消除轮询开销。

Python串口通信怎么避免阻塞?多线程与异步读取实现方案
图3:asyncio 异步事件驱动串口读取流程图

异步协程读取标准实现代码

import asyncio
import aioserial

async def read_serial_loop(port: str, baudrate: int):
    # 初始化异步串口对象
    ser = aioserial.AioSerial(port=port, baudrate=baudrate)
    print(f"已异步接入串口: {port}")
    try:
        while True:
            # 挂起当前协程,直到读到一整行或超时,底层完全交由事件循环调度
            line = await ser.readline_async()
            if line:
                payload = line.decode(errors="ignore").strip()
                print(f"[异步接收] 数据: {payload}")
                
                # 可以在此处直接触发其他异步操作,例如推送到网络WebSocket或数据库
                await asyncio.sleep(0)  # 礼让执行权
    except asyncio.CancelledError:
        print("异步读取任务取消...")
    finally:
        ser.close()
        print("串口已关闭。")

async def heartbeat():
    # 模拟主应用中并行运行的其他异步任务
    while True:
        print("[主循环] 心跳正常运行,无阻塞...")
        await asyncio.sleep(2.0)

async def main():
    # 并发运行串口读取与主业务协程
    serial_task = asyncio.create_task(read_serial_loop("/dev/ttyUSB0", 115200))
    heartbeat_task = asyncio.create_task(heartbeat())
    
    # 运行5秒作为演示
    await asyncio.sleep(5)
    serial_task.cancel()
    heartbeat_task.cancel()
    await asyncio.gather(serial_task, heartbeat_task, return_exceptions=True)

if __name__ == "__main__":
    asyncio.run(main())

四、方案对比与技术选型总结

多线程与异步IO各有明确的技术适配边界。对于绝大多数传统工程,多线程方案依靠 pyserial 原生支持,不需要引入额外的第三方依赖;而在面对现代高并发微服务或物联网接入平台时,asyncio 方案具备更好的资源利用率。

Python串口通信怎么避免阻塞?多线程与异步读取实现方案
图4:两种架构核心特征与适用场景评估总结
比对维度 基础超时轮询 多线程 + Queue asyncio 异步协程
主逻辑解耦 未解耦(主循环兼任) 完全解耦(独立线程) 完全解耦(事件驱动)
CPU占用率 容易因空转偏高 极低(阻塞/微休眠) 极低(内核通知唤醒)
扩展复杂度 低 中等(需维护线程生命周期) 中等(需纯异步生态协同)
典型应用 简易调试脚本 桌面客户端、上位机 网关服务、并发设备接入
工程实践注意事项:
  • 物理拔出异常处理:在USB转串口拔出时,无论是多线程还是异步协程均会抛出 SerialException。务必在外层做重连保护机制,避免整个进程崩退。
  • 分包与黏包处理:串口是流式协议,read() 得到的字节可能是不完整帧。对于固定协议头尾(如 0xAA 0x55),建议在入队前或消费端实现专门的状态机进行拼帧。
相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。