Python Tkinter串口多线程读写是否存在竞争条件问题?
解决多线程串口读取时关闭连接引发的TypeError问题
问题描述
在Python3 Tkinter项目中,使用独立线程读取Arduino通过USB发送的毫秒数据流(避免阻塞GUI循环),通过GUI控制串口连接与断开时,断开操作会触发如下错误:
Exception in thread Thread-1 (serialStream): Traceback (most recent call last): File "/usr/lib/python3.10/threading.py", line 1016, in _bootstrap_inner self.run() File "/usr/lib/python3.10/threading.py", line 953, in run self._target(*self._args, **self._kwargs) File "/home/soon/Python/mwe_threaded_serial.py", line 12, in serialStream rawReading = str(serialConnection.readline()) File "/home/soon/.local/lib/python3.10/site-packages/serial/serialposix.py", line 575, in read buf = os.read(self.fd, size - len(read)) TypeError: 'NoneType' object cannot be interpreted as an integer
预期行为是主线程调用serialConnection.close()后,读取线程会进入等待重连的状态,但实际仍在执行readline()并触发错误。这属于多线程竞争条件问题,以下是解决方案:
最小复现代码
Python代码
import serial import threading import time # 修改为你的系统对应的串口 serialPort = "/dev/ttyACM0" serialConnection = serial.Serial() def serialStream(): while True: if (serialConnection.is_open): rawReading = str(serialConnection.readline()) print(rawReading) else: pass def connect_serial(): global serialConnection, serialPort # 如果已有连接则先关闭 if (serialConnection.is_open): serialConnection.close() time.sleep(1) serialConnection = serial.Serial( port=serialPort,\ baudrate=9600,\ parity=serial.PARITY_NONE,\ stopbits=serial.STOPBITS_ONE,\ bytesize=serial.EIGHTBITS,\ timeout=1) time.sleep(1) if (not serialConnection.is_open): print("Connection failed") else: print("Connection establied") thread = threading.Thread(target=serialStream) thread.daemon = True thread.start() connect_serial() time.sleep(5) serialConnection.close() time.sleep(5) connect_serial()
Arduino代码
void setup() { // 初始化串口通信,波特率9600 Serial.begin(9600); } void loop() { Serial.println(millis()); delay(1); // 短暂延迟保证稳定性 }
问题根源
这是典型的多线程竞争条件:
- 读取线程执行
if (serialConnection.is_open)判断时,连接仍处于打开状态,进入读取分支; - 主线程此时调用
serialConnection.close(),导致串口内部的文件描述符fd变为None; - 读取线程执行
readline()时,底层调用os.read()使用了已变为None的fd,触发TypeError。 - 此外
connect_serial中直接重新赋值serialConnection的操作,会导致读取线程可能引用到旧的已关闭对象,进一步加剧问题。
解决方案
1. 引入线程安全的状态标记
使用共享布尔变量标记连接状态,配合线程锁确保状态读写的原子性,避免依赖串口对象的is_open属性判断。
2. 捕获串口操作异常
在读取线程中捕获串口相关异常,保证线程不会因连接关闭而崩溃,而是进入等待状态。
3. 复用串口对象
避免直接替换serialConnection对象,而是复用原有对象修改参数后重新打开,避免线程引用不一致。
修改后的完整代码
import serial import threading import time serialPort = "/dev/ttyACM0" serialConnection = serial.Serial() # 线程安全的连接状态标记,配合锁使用 is_connected = False conn_lock = threading.Lock() def serialStream(): global is_connected, serialConnection while True: # 线程安全地读取连接状态 with conn_lock: current_connected = is_connected if current_connected: try: # 二次确认连接状态,避免锁释放后状态变化 if serialConnection.is_open: # 读取并解码数据,避免str()转换带来的冗余字符 rawReading = serialConnection.readline().decode('utf-8').strip() if rawReading: print(rawReading) except (serial.SerialException, TypeError) as e: print(f"读取数据出错: {e}") # 出错时标记为未连接 with conn_lock: is_connected = False # 短暂休眠避免空循环占用CPU time.sleep(0.1) else: # 未连接时休眠,降低CPU消耗 time.sleep(0.5) def connect_serial(): global serialConnection, is_connected, serialPort # 先关闭现有连接 with conn_lock: if is_connected: serialConnection.close() is_connected = False time.sleep(1) try: # 复用原有串口对象,避免重新赋值 serialConnection.port = serialPort serialConnection.baudrate = 9600 serialConnection.parity = serial.PARITY_NONE serialConnection.stopbits = serial.STOPBITS_ONE serialConnection.bytesize = serial.EIGHTBITS serialConnection.timeout = 1 serialConnection.open() time.sleep(1) # 更新连接状态 with conn_lock: is_connected = serialConnection.is_open print("连接成功" if is_connected else "连接失败") except serial.SerialException as e: print(f"连接出错: {e}") thread = threading.Thread(target=serialStream) thread.daemon = True thread.start() connect_serial() time.sleep(5) # 先标记状态为未连接,再关闭串口 with conn_lock: is_connected = False serialConnection.close() time.sleep(5) connect_serial()
关键修改说明
is_connected和conn_lock确保主线程与读取线程对连接状态的操作是线程安全的,避免竞争;- 读取线程中增加异常捕获,处理连接关闭时的读取错误,保证线程持续运行;
- 复用
serialConnection对象,避免重新赋值导致的对象引用不一致问题; - 未连接状态下线程休眠,减少CPU资源占用。
内容的提问来源于stack exchange,提问作者Søren ONeill
相关产品推荐
相关产品推荐

