QT下串口通信线程实现周期发送与触发接收的正确架构方案
Qt串口通信线程正确架构方案
现有方案的核心问题
你当前的实现存在几个关键缺陷,无法稳定运行:
- 继承QThread重写run的逻辑里是无间隔死循环,会占满单核心CPU,同时没有调用
exec()启动线程事件循环,你定义的QTimer、QSerialPort的信号完全无法触发,绑定的槽函数不会执行 - 用互斥锁tryLock作为任务触发标记的逻辑完全冗余,且代码中
tryLock后再次调用lock会直接触发死锁(非递归QMutex不允许同一线程重复加锁) - 串口、定时器对象的初始化和操作跨线程,会触发Qt的线程安全警告,甚至直接崩溃
推荐架构实现要点
Qt官方更推荐Worker对象+moveToThread的方案实现异步任务线程,天生适配你需要的周期性任务+触发式任务的需求,无需手动用锁模拟触发逻辑:
- 抽离所有通信逻辑到独立的Worker类,仅继承QObject即可,不需要继承QThread
- 主线程创建QThread实例和Worker实例,调用
moveToThread将Worker绑定到子线程,所有Worker的槽函数都会在子线程上下文执行 - 周期性发送任务直接用Worker内部的QTimer实现,timeout信号绑定发送逻辑槽,设置对应周期即可自动触发
- 触发式接收任务直接用QSerialPort的
readyRead信号绑定接收解析槽,有数据到达时自动执行 - 共享通信结构体的访问统一加QMutex保护:
- 子线程写、主线程读的字段(返回状态、反馈角度):子线程更新时加锁,主线程读取时也加锁
- 主线程写、子线程读的字段(发送参数、配置信息):要么通过跨线程信号槽传递(队列连接自动保证线程安全),要么加锁访问
- 不允许丢失的位置数据,每次提取后直接通过信号发往主线程绘图控件,Qt的队列连接会自动缓存所有信号不会丢失,非敏感状态数据可以攒固定间隔再发信号更新界面,降低主线程压力
代码示例
通信Worker类实现
#include <QObject> #include <QSerialPort> #include <QTimer> #include <QMutex> #include <QMutexLocker> // 提前注册自定义类型用于跨线程信号传递 Q_DECLARE_METATYPE(Communication_Packets_st) class CommWorker : public QObject { Q_OBJECT public: explicit CommWorker(QObject *parent = nullptr) : QObject(parent) { m_serial = new QSerialPort(this); m_sendTimer = new QTimer(this); // 内部信号槽绑定,都在子线程执行 connect(m_sendTimer, &QTimer::timeout, this, &CommWorker::handleSendTask); connect(m_serial, &QSerialPort::readyRead, this, &CommWorker::handleRecvTask); m_commMutex = new QMutex(QMutex::Recursive); } ~CommWorker() { if(m_serial->isOpen()) m_serial->close(); delete m_commMutex; } public slots: // 子线程启动后执行初始化,保证所有IO对象在子线程上下文创建 void initPort(QString portName, int baudRate, int sendIntervalMs) { m_periodSec = sendIntervalMs / 1000.0; m_serial->setPortName(portName); m_serial->setBaudRate(baudRate); m_serial->setDataBits(QSerialPort::Data8); m_serial->setParity(QSerialPort::NoParity); m_serial->setStopBits(QSerialPort::OneStop); if(m_serial->open(QIODevice::ReadWrite)) { m_sendTimer->start(sendIntervalMs); } else { emit errorOccurred(m_serial->errorString()); } } // 主线程更新发送参数的入口,线程安全 void updateSendAngle(double angle) { QMutexLocker locker(m_commMutex); m_sendAngle = angle; } // 主线程读取全量状态的入口,线程安全 void getFullStatus(Communication_Packets_st *outStatus) { QMutexLocker locker(m_commMutex); memcpy(outStatus, &m_commManager, sizeof(Communication_Packets_st)); } private slots: // 周期性发送任务 void handleSendTask() { QMutexLocker locker(m_commMutex); // 填充发送报文 m_commManager.ControlCommandPacket_s.CommandAngle_i32 = static_cast<int32_t>(m_sendAngle * COMMAND_ANGLE_MULTIPLIER); // 调用发送接口 Comm_SendControlCommand(&m_commManager); // 发信号通知主线程更新发送位置,不会丢失 emit sendPosUpdated(m_sendIndex * m_periodSec, m_sendAngle); m_sendIndex++; } // 触发式接收任务 void handleRecvTask() { QByteArray recvBuf = m_serial->readAll(); QMutexLocker locker(m_commMutex); // 解析报文 Comm_PacketParser(&m_commManager, recvBuf.constData(), recvBuf.size()); // 提取反馈位置发往主线程 double recvAngle = static_cast<double>(m_commManager.ControlCommandResponsePacket_s.AxisAngle_i32) * FEEDBACK_ANGLE_MULTIPLIER; emit recvPosUpdated(m_sendIndex * m_periodSec, recvAngle); } signals: void sendPosUpdated(double time, double angle); void recvPosUpdated(double time, double angle); void errorOccurred(QString errMsg); private: QSerialPort *m_serial; QTimer *m_sendTimer; QMutex *m_commMutex; Communication_Packets_st m_commManager; double m_sendAngle = 0.0; double m_periodSec = 0.01; uint32_t m_sendIndex = 0; };
主线程调用示例
// 主线程头文件中声明成员 QThread *m_commThread; CommWorker *m_commWorker; // 主线程初始化逻辑 void MainWindow::initCommThread() { qRegisterMetaType<Communication_Packets_st>(); m_commThread = new QThread(this); m_commWorker = new CommWorker(); m_commWorker->moveToThread(m_commThread); // 绑定信号槽 connect(m_commThread, &QThread::started, this, [=](){ m_commWorker->initPort("COM3", 115200, 10); // 10ms间隔对应100Hz发送频率 }); // 位置数据更新绑定到绘图控件 connect(m_commWorker, &CommWorker::sendPosUpdated, ui->plotWidget, &PlotWidget::addSendPoint); connect(m_commWorker, &CommWorker::recvPosUpdated, ui->plotWidget, &PlotWidget::addRecvPoint); // 自动回收资源 connect(m_commThread, &QThread::finished, m_commWorker, &QObject::deleteLater); connect(m_commThread, &QThread::finished, m_commThread, &QObject::deleteLater); m_commThread->start(); } // 窗口关闭时退出线程 void MainWindow::closeEvent(QCloseEvent *event) { if(m_commThread && m_commThread->isRunning()) { m_commThread->quit(); m_commThread->wait(1000); } event->accept(); }
注意事项
- 不要在主线程直接操作Worker内部的QSerialPort、QTimer对象,所有操作都通过槽函数触发
- 自定义类型如果要作为跨线程信号的参数,必须提前用
qRegisterMetaType注册 - 尽量用
QMutexLocker自动管理锁的生命周期,避免异常场景下锁未释放导致死锁
内容的提问来源于stack exchange,提问作者neko
相关产品推荐
相关产品推荐

