基于QT的上位机软件,和下位机一般都存在通信。 但如果是在主窗体类里面实现通信,往往会和主线程争抢CPU,导致通信非常容易出现异常。 最好的方式是给通信程序单独开一个线程来实现,而主窗体类(主线程)则主要专注于用户的交互逻辑。本文将以串口通信为例,来讲解一下如何单独为串口通信开一个线程。
正文
QT直接与多线程相关的内容是比较多的,说实话本人也没怎么看,也不是本文重点,本文介绍的开线程的方式,可能是有点讨巧的方式,但这种方式用起来还是很稳定的,之所以学到这种方式,估计也是因为这种方式广大网友用的多,比较流行。
QObject类有一个函数是 moveToThread,帮助文档的介绍如下
这个函数它主要的作用的就是改变本对象及其子对象的线程相关性。 但它要求了调用这函数的对象是不能有父对象,如果有父对象那就无法改变线程相关性。
据我了解,好像Qt里面的绝大部分类(还是所有的类)都是继承自QObject的,所以呢,串口通信类QSerialPort肯定也是继承自QObject的,那么它同样呢,也是可以调用moveToThread这个函数的。那本文提到的开线程方式呢,就是自己创建一个类“ESerialPort”,然后继承自QSerialPort。然后调用moveToThread函数,把它自己移动到线程当中去。好的,下面给出具体的参考代码如下:
首先我们来看自己定义的ESerialPort的.h文件
#ifndef ESERIALPORT_H
#define ESERIALPORT_H
#include <QtCore/QObject>
#include <QSerialPort>
class ESerialPort : public QSerialPort
{
Q_OBJECT
public:
ESerialPort(QObject *parent = nullptr);
signals:
public slots:
private:
};
#endif // ESERIALPORT_H
然后是 .cpp文件
#include "eserialport.h"
ESerialPort::ESerialPort(QObject *parent) : QSerialPort(parent)
{
}
这个类可以具有QSerialPort的所有功能,同时还能扩充一些自定义的功能。
好,那么我们来看看,具体怎么把这个类弄到线程中去,请看下面的参考代码。
我们的主窗体类的名字叫fugu哈,我自己取的,含义不用深究
.h文件
#ifndef FUGU_H
#define FUGU_H
#include <QMainWindow>
#include "eserialport.h"
#include <QThread>
namespace Ui {
class fugu;
}
class fugu : public QMainWindow
{
Q_OBJECT
public:
explicit fugu(QWidget *parent = nullptr);
~fugu();
signals:
private slots:
private:
Ui::fugu *ui;
ESerialPort *m_pserial_port;
QThread * m_pthread_serial_port;
};
#endif // FUGU_H
注意看,在主窗体类里面,我们在private里面定义了两个私有变量,等会将会在初始化函数中用到。
ESerialPort *m_pserial_port;
QThread * m_pthread_serial_port;
.cpp文件
#include "fugu.h"
#include "ui_fugu.h"
#include <stack>
#include <stack.h>
fugu::fugu(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::fugu)
{
ui->setupUi(this);
// 串口通信单独开一个线程
m_pserial_port = new ESerialPort;
m_pthread_serial_port = new QThread(this);
m_pserial_port->moveToThread(m_pthread_serial_port);
m_pthread_serial_port->start();
connect(this, &QObject::destroyed,[=](){
m_pthread_serial_port->exit();
m_pthread_serial_port->wait();
delete m_pserial_port;
});
}
fugu::~fugu()
{
delete ui;
}
第一点要注意的是这行代码
m_pserial_port = new ESerialPort;
它有没有指定父对象, 是不是没有? 前面我们提到了,它是不能指定父对象的,如果指定父对象了,那就无法实现moveToThread的功能。
继续往下看
m_pthread_serial_port = new QThread(this);
但是咱们的线程对象是可以指定父对象的,为什么呢? A QThread object manages one thread of control within the program. 它本身就是用来管理线程的。感觉指定父对象可以这样去理解,就是fugu是主线程,m_pthread_serial_port是fugu的子线程。 它后面还专门提到了用QObject::moveToThread()这个函数把worker objects移动进来。那么本例的worker objects是什么呢?就是自定义类ESerialPort的对象m_pserial_port。
所以,就有了第三行
m_pserial_port->moveToThread(m_pthread_serial_port);
第四行是开启这个线程没什么好说的
m_pthread_serial_port->start();
最后这个connect也是需要关注的,目的就是在主窗体类关闭的时候,给这个线程释放资源,以前我总是忽略这点,关闭程序的时候总是提示异常。就是因为没有加这一行。 大家也不必纠结,直接照抄就行了(未来的我也会直接照抄🙂)。
connect(this, &QObject::destroyed,[=](){
m_pthread_serial_port->exit();
m_pthread_serial_port->wait();
delete m_pserial_port;
});
不知道大家注意到没有,无论是主窗口类还是自定义串口类,它们的.h文件中,都有
signals: public slots:
熟悉它们的朋友肯定知道信号函数与槽函数的声明就得放在它们下面。具体的内容将在另一篇QT学习_15_线程间通信以及注意事项-CSDN博客继续讲解,欢迎阅读。