这几天在研究如何使用Qt的多线程,想将串口操作放到线程中去执行,这样的话,就算是串口接收大量的数据,也不会导致界面出现假死的现象。
之前在使用串口的时候,一般都是采用异步(非阻塞)方式通信,也即是通过调用:
connect(serial, &QSerialPort::readyRead, this, &SerialBoard::readData);
//直接读取全部数据
void SerialBoard::readData()
{
rxData = serial->readAll();
}
采用异步(非阻塞)通信其实可以不需要使用线程操作,因为接受数据是异步的,所以不会造成界面的堵塞,但是异步通信会有一个问题就是,当数据量比较多的时候,可能会多次触发readyRead信号,这样就会把数据分割了。而实际情况下,我们总是希望一次读取完所有的数据,那么这个时候我们就可以使用 同步(阻塞)通信方式了。
在GUI应用程序中,为了避免冻结用户界面,阻塞串行端口只能在非GUI线程中使用。
同步(阻塞)方法:在非gui和多线程应用程序中,可以调用waitFor…()函数(例如QSerialPort::waitForReadyRead())来挂起调用线程,直到操作完成。
下面就来说一说,我们使用多线程中遇到的问题:
- QObject::moveToThread: Cannot move objects with a parent.
- QObject: Cannot create children for a parent that is in a different thread.
(Parent is QSerialPort(0x1c6deb18), parent’s thread is QThread(0x205e180), current thread is SAKSerialPortDevice(0x1c60de48)
其实,这两个问题,应该是多线程中最常见的两个问题了。
出现这些问题的根本原因是没有正确是使用多线程操作。
在代码中,我们是这样实现的:
通过自定义线程类继承QThread。在构造函数中,执行 moveToThread(this). 打算将该类的所有函数都移动到线程中执行。(也就是除了run函数外,继承QThread类的其他函数也运行在单独的线程中)。
但是,官方提示“不建议这样操作”
每个 QObject 的对象,都和某个创建对象所在的线程关联。如果把对象通过 moveToThread 移动到其他线程,这个对象不能有父对象。否则就会出现 QObject::moveToThread: Cannot move objects with a parent
在代码中由于我们指定了 parent 为 this.所以出现了这个警告。
因此,不推荐这样使用。
正确的使用多线程
Qt 有两种方法多线程的方法,其中一种就是继承 QThread 的 run 函数,另外一种就是把一个继承QObject 的类转移到一个 QThread 里。
Qt4.8 之前都是使用继承 QThread 的 run方法,但是4.8之后,Qt官方建议使用第二种。两种方法差别不大,用起来都比较方便,但继承 QObject 的方法更加灵活。
关于如何使用这两种方法,我这里就不描述了,这位博主比我讲的更好:
Qt使用多线程的一些心得——1.继承QThread的多线程使用方法
Qt使用多线程的一些心得——2.继承QObject的多线程使用方法
我这里只是做一点总结,总结我在先写过程中遇到的问题与需要注意的地方。
-
继承QThread 的类,除了 run 函数在单独的线程中执行,其余函数都与创建 继承QThread的类 在同一个线程。
这里是在 MainWindow 里面创建多线程。所以,MyThread 的其他非 run 函数都在和 mainwindow 在同一个线程。 -
QObject的线程转移函数是:void moveToThread(QThread * targetThread) ,通过此函数可以把一个顶层Object(就是没有父级)转移到一个新的线程里。
- 用QObject来实现多线程有个非常好的优点,就是默认就支持事件循环(Qt的许多非GUI类也需要事件循环支持,如QTimer、QTcpSocket),QThread要支持事件循环需要在QThread::run()中调用QThread::exec()来提供对消息循环的支持,否则那些需要事件循环支持的类都不能正常发送信号,因此如果要使用信号和槽,那就直接使用QObject来实现多线程。
- 创建及销毁线程。
继承QObject多线程的方法线程的创建很简单,只要让QThread的start函数运行起来就行,但是需要注意销毁线程的方法 。
在线程创建之后,这个QObject的销毁不应该在主线程里进行,而是通过deleteLater槽进行安全的销毁,因此,继承QObject多线程的方法在创建时有几个槽函数需要特别关注:
(1). 一个是QThread的finished信号对接QObject的deleteLater使得线程结束后,继承QObject的那个多线程类会自己销毁。
(2). 另一个是QThread的finished信号对接QThread自己的deleteLater,这个不是必须,如果 QThread 也是用堆分配 (定义全局的指针 QThread *m_objThread),这样,让QThread自杀的槽就一定记得加上,否则QThread就逍遥法外了。
/*
在头文件中定义了 QThread *m_objThread
*/
m_objThread= new QThread();
connect(m_objThread,&QThread::finished,m_objThread,&QObject::deleteLater);
使用QObject创建多线程的方法如下:
- 写一个继承QObject的类,对需要进行复杂耗时逻辑的入口函数声明为槽函数。
- 此类在旧线程new出来,不能给它设置任何父对象。
- 同时声明一个QThread对象,在官方例子里,QThread并没有new出来,这样在析构时就需要调用QThread::wait(),如果是堆分配的话, 可以通过deleteLater来让线程自杀。
- 把obj通过moveToThread方法转移到新线程中,此时object已经是在线程中了.
- 把线程的finished信号和object的deleteLater槽连接,这个信号槽必须连接,否则会内存泄漏
- 正常连接其他信号和槽(在连接信号槽之前调用moveToThread,不需要处理connect的第五个参数,否则就显示声明用Qt::QueuedConnection来连接)
- 初始化完后调用’QThread::start()'来启动线程
- 在逻辑结束后,调用QThread::quit退出线程的事件循环。
下面是使用串口多线程的代码:
MySerialPortObject.h
#ifndef MYSERIALPORTOBJECT_H
#define MYSERIALPORTOBJECT_H
#include <QObject>
#include <QSerialPort>
#include <QSerialPortInfo>
class MySerialPortObject : public QObject
{
Q_OBJECT
public:
explicit MySerialPortObject(QObject *parent = 0);
~MySerialPortObject();
void initConnnect();
bool isOpen();
QSerialPort *serialPort;
signals:
void signal_massgae(QString str);
void signal_portStatusChangle(bool flag);
void signal_bytesRead(QByteArray data);
void signal_bytesWriten(QByteArray data);
void signal_deletePortObject();
public slots:
/* 读数据 */
void slot_readBytes();
/* 写数据 */
void slot_writeBytes(QByteArray data);
void slot_process();
/* open */
void open(QString portName,qint32 baudRate);
/* close */
void close();
private:
qint64 waitForReadyReadTime = 100;
qint64 waitForBytesWrittenTime = 5;
};
#endif // MYSERIALPORTOBJECT_H
MySerialPortObject.cpp
#include "myserialportobject.h"
#include <QThread>
#include <QDebug>
//#define __DEBUG
MySerialPortObject::MySerialPortObject(QObject *parent) : QObject(parent)
{
#ifdef __DEBUG
qDebug()<<__FUNCTION__<< "thread: "<<QThread::currentThread();
#endif
}
MySerialPortObject::~MySerialPortObject()
{
qDebug()<<"执行析构";
emit signal_deletePortObject();
close();
delete this->serialPort;
}
void MySerialPortObject::slot_process()
{
#ifdef __DEBUG
qDebug()<<__FUNCTION__<< "thread: "<<QThread::currentThread();
#endif
this->serialPort = new QSerialPort;
initConnnect();
}
void MySerialPortObject::initConnnect()
{
connect(serialPort,SIGNAL(readyRead()),this,SLOT(slot_readBytes()));
}
void MySerialPortObject::close()
{
#ifdef __DEBUG
qDebug()<<__FUNCTION__<< "thread: "<<QThread::currentThread();
#endif
if(this->serialPort->isOpen()) {
this->serialPort->close();
}
}
void MySerialPortObject::open(QString portName, qint32 baudRate)
{
#ifdef __DEBUG
qDebug()<<__FUNCTION__<< "thread: "<<QThread::currentThread();
#endif
this->serialPort->setPortName(portName);
this->serialPort->setBaudRate(baudRate);
this->serialPort->setDataBits(QSerialPort::Data8);
this->serialPort->setStopBits(QSerialPort::OneStop);
this->serialPort->setParity(QSerialPort::NoParity);
if(this->serialPort->open(QIODevice::ReadWrite)){
emit signal_portStatusChangle(true);
} else {
emit signal_portStatusChangle(false);
emit signal_massgae(QString("open failed: %1").arg(this->serialPort->errorString()));
}
}
void MySerialPortObject::slot_readBytes()
{
#ifdef __DEBUG
qDebug()<<__FUNCTION__<< "thread: "<<QThread::currentThread();
#endif
serialPort->waitForReadyRead(waitForReadyReadTime);
QByteArray data = serialPort->readAll();
if (data.isEmpty()){
return;
}
emit signal_bytesRead(data);
}
void MySerialPortObject::slot_writeBytes(QByteArray data)
{
#ifdef __DEBUG
qDebug()<<__FUNCTION__<< "thread: "<<QThread::currentThread();
#endif
qint64 ret = serialPort->write(data);
serialPort->waitForBytesWritten(waitForBytesWrittenTime);
if (ret == -1){
emit signal_massgae(QString("send failed: %1").arg(this->serialPort->errorString()));
}else{
emit signal_bytesWriten(data);
}
}
bool MySerialPortObject::isOpen()
{
return this->serialPort->isOpen();
}
Mainwindow.cpp
portThread = new QThread();
portObject = new MySerialPortObject();
//类移动到线程中执行
portObject->moveToThread(portThread);
connect(portObject,SIGNAL(signal_massgae(QString)),this,SLOT(slot_massage(QString)));
connect(portObject,SIGNAL(signal_deletePortObject()),portThread,SLOT(quit()));//对象删除时,调用QThread::quit退出线程的事件循环
connect(portObject,SIGNAL(signal_deletePortObject()),portThread,SLOT(deleteLater()));//对象删除时,内存释放
connect(portThread,&QThread::finished,portThread,&QObject::deleteLater);
connect(portThread,SIGNAL(finished()),portObject,SLOT(deleteLater()));//线程结束后,继承QObject的那个多线程类会自己销毁,否则会内存泄漏。
connect(portThread,SIGNAL(started()),portObject,SLOT(slot_process()));
connect(portObject,SIGNAL(signal_bytesRead(QByteArray)),this,SLOT(slot_bytesRead(QByteArray)));
connect(this,SIGNAL(signal_writeDataRequest(QByteArray)),portObject,SLOT(slot_writeBytes(QByteArray)));
connect(this,SIGNAL(signal_serilaPortOpen(QString,qint32)),portObject,SLOT(open(QString,qint32)));
connect(this,SIGNAL(signal_serilaPortClose()),portObject,SLOT(close()));
portThread->start();
在这里可以看出,Mainwindow 与 MySerialPortObject 通信都是通过 connect()槽连接的方式进行的,因为 connect 的第五个参数会自动判断线程连接方式。
我们在 Mainwindow中 创建了 MySerialPortObject 类,所有,执行 MySerialPortObject 的构造函数与 Mainwindow 处于用一个线程,刚开始的时候,我将
MySerialPortObject::MySerialPortObject(QObject *parent) : QObject(parent)
{
#ifdef __DEBUG
qDebug()<<__FUNCTION__<< "thread: "<<QThread::currentThread();
#endif
this->serialPort = new QSerialPort;
initConnnect();
}
这两句放在构造函数中,结果直接导致
QObject: Cannot create children for a parent that is in a different thread.
(Parent is QSerialPort(0x66c1f50), parent’s thread is QThread(0x2b7e270), current thread is QThread(0x66d00d8)
翻译:
QObject:无法为处于不同线程中的父级创建子级。
父级是QSerialPort(0x66c1f50),父级线程是QThread(0x2b7e270),当前线程是QThread(0x66d00d8)
这是因为,this->serialPort 对象 在构造函数中创建,和 Mainwindow 处于同一线程。在执行
void MySerialPortObject::slot_writeBytes(QByteArray data)
{
#ifdef __DEBUG
qDebug()<<__FUNCTION__<< "thread: "<<QThread::currentThread();
#endif
qint64 ret = serialPort->write(data);
serialPort->waitForBytesWritten(waitForBytesWrittenTime);
if (ret == -1){
emit signal_massgae(QString("send failed: %1").arg(this->serialPort->errorString()));
}else{
emit signal_bytesWriten(data);
}
}
函数时,是处于新线程中,使用了 serialPort 对象,二者处于不同的线程。所以报错。
因此需要 在
connect(portThread,SIGNAL(started()),portObject,SLOT(slot_process()));
线程启动的时候,执行槽函数,
void MySerialPortObject::slot_process()
{
#ifdef __DEBUG
qDebug()<<__FUNCTION__<< "thread: "<<QThread::currentThread();
#endif
this->serialPort = new QSerialPort;
initConnnect();
}
使得二者处于同一线程。
并且 当在 Mainwindow 函数中,不通过 信号槽连接调用 MySerialPortObject 类 中的函数也会报
QObject: Cannot create children for a parent that is in a different thread.
(Parent is QSerialPort(0x66c1f50), parent’s thread is QThread(0x2b7e270), current thread is QThread(0x66d00d8)
因为 Mainwindow 中调用 MySerialPortObject 类中的函数会与执行该函数的线程与 Mainwindow线程一致,这样就不是多线程了。
因此需要执行 MySerialPortObject 类中的函数,必须通过信号槽连接。
这里还需要注意,开辟了新的线程,需要注意内存的泄漏问题。刚开始的时候,我的程序每次内存都会增加4kB, 运行时间久了,就会导致程序崩溃,导致内存溢出的现象。
//对象删除时,内存释放
connect(portObject,SIGNAL(signal_deletePortObject()),portThread,SLOT(deleteLater()));
//QThread 是堆分配 ,让QThread自杀的槽就一定记得加上,否则QThread就逍遥法外了。
connect(portThread,&QThread::finished,portThread,&QObject::deleteLater);
//线程结束后,继承QObject的那个多线程类会自己销毁,否则会内存泄漏。
connect(portThread,SIGNAL(finished()),portObject,SLOT(deleteLater()));
执行这一系列的操作就是为了避免内存的泄漏问题。
在Mainwindow析构函数中添加:
MainWindow::~MainWindow()
{
qDebug() << "start destroy widget";
if(portThread)
{
portThread->quit();
}
portThread->wait();
qDebug() << "end destroy widget";
delete ui;
}