Qt 多线程使用总结

这几天在研究如何使用Qt的多线程,想将串口操作放到线程中去执行,这样的话,就算是串口接收大量的数据,也不会导致界面出现假死的现象。

之前在使用串口的时候,一般都是采用异步(非阻塞)方式通信,也即是通过调用:

connect(serial, &QSerialPort::readyRead, this, &SerialBoard::readData);

//直接读取全部数据
void SerialBoard::readData()
{
    rxData = serial->readAll();
} 

采用异步(非阻塞)通信其实可以不需要使用线程操作,因为接受数据是异步的,所以不会造成界面的堵塞,但是异步通信会有一个问题就是,当数据量比较多的时候,可能会多次触发readyRead信号,这样就会把数据分割了。而实际情况下,我们总是希望一次读取完所有的数据,那么这个时候我们就可以使用 同步(阻塞)通信方式了。

在GUI应用程序中,为了避免冻结用户界面,阻塞串行端口只能在非GUI线程中使用。

同步(阻塞)方法:在非gui和多线程应用程序中,可以调用waitFor…()函数(例如QSerialPort::waitForReadyRead())来挂起调用线程,直到操作完成。

下面就来说一说,我们使用多线程中遇到的问题:

  1. QObject::moveToThread: Cannot move objects with a parent.
  2. 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)

其实,这两个问题,应该是多线程中最常见的两个问题了。

出现这些问题的根本原因是没有正确是使用多线程操作。

在代码中,我们是这样实现的:
Qt 多线程使用总结_第1张图片
通过自定义线程类继承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的多线程使用方法

我这里只是做一点总结,总结我在先写过程中遇到的问题与需要注意的地方。

  1. 继承QThread 的类,除了 run 函数在单独的线程中执行,其余函数都与创建 继承QThread的类 在同一个线程。
    Qt 多线程使用总结_第2张图片
    这里是在 MainWindow 里面创建多线程。所以,MyThread 的其他非 run 函数都在和 mainwindow 在同一个线程。

  2. QObject的线程转移函数是:void moveToThread(QThread * targetThread) ,通过此函数可以把一个顶层Object(就是没有父级)转移到一个新的线程里。

  • 用QObject来实现多线程有个非常好的优点,就是默认就支持事件循环(Qt的许多非GUI类也需要事件循环支持,如QTimer、QTcpSocket),QThread要支持事件循环需要在QThread::run()中调用QThread::exec()来提供对消息循环的支持,否则那些需要事件循环支持的类都不能正常发送信号,因此如果要使用信号和槽,那就直接使用QObject来实现多线程。
  1. 创建及销毁线程。
    继承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 
#include 
#include 


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 
#include 
//#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;
}

你可能感兴趣的:(qt)