You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);  // 短暂延迟保证稳定性
}

问题根源

这是典型的多线程竞争条件:

  1. 读取线程执行if (serialConnection.is_open)判断时,连接仍处于打开状态,进入读取分支;
  2. 主线程此时调用serialConnection.close(),导致串口内部的文件描述符fd变为None;
  3. 读取线程执行readline()时,底层调用os.read()使用了已变为None的fd,触发TypeError。
  4. 此外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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 23:45:48