一、典型的问题:

在我们的多线程编程中,一般主线程负责界面的刷新显示,而子线程负责一些耗时的操作,例如当我们使用QSerialPort的时候,我们希望QSerialPort的读写操作在子线程中进行。

QObject: Cannot create children for a parent that is in a different thread.

那么如果在主线程中创建QSerialPort对象,通过指针的形式将QSerialPort传入到了子线程中,在子线程的函数内使用QSerialPort指针操作读写函数,就会出现上面的错误提示!

原因:在主线程中创建了QSerialPort对象在子线程中调用,或者在子线程中创建然后在主线程中调用了。这就是跨线程调用引起的问题!

对于继承QThread重写run函数的情况,往往容易在run外部定义QSerialport *port = new QSerialport()对象,然后在run中调用port->readAll()等函数,然而根据QThread的特性,只有run函数才运行在新的子线程中,所以这里就跨线程调用了 QSerialport对象,会出现上述报错。

**注意:**其实不止是QSerialPort,只要是QIODevice类型的对象都无法进行跨线程调用,这里是由于Qt的信号和槽机制的问题,再有就是QSerialPort是异步的,什么意思呢?就是说使用QSerialPort->readData()函数的时候不会阻塞,会立刻返回,其实QSerialPort->readData()只是在读取一个缓冲区而已,而在另一个线程中,有数据的话就会将数据写入到这个缓冲区。

解决办法:

派生类外部定义QSerialPort指针,在run函数中再定义对象,并且数据的读、写都在run函数中进行。

上述方法可以解决报错,但是针对不仅仅具有读串口数据需求、还需要向串口写数据的情况,显然这样的方法会让run函数变得十分臃肿,代码的维护十分麻烦。

推荐办法:
运用继承QObject,结合MoveToThread()的方式:

自定义my_serial类,继承自QObject,将一个QThread local_thread 对象作为派生类的成员,使用QObject的MoveToThread()将派生类自己以及QSerialPort对象都移动到local_thread线程中,这样派生类中的槽函数以及QSerialPort中的信号与槽函数都将在local_thread线程执行。

1.解决跨线程调用
既然无法通过指针的形式将QSerialPort传入子线程中,那么就只有在子线程中新建QSerialPort对象。

显然,如果使用第一种多线程方式的话,由于只有run()函数内的代码是在子线程的,那么QSerialPort就只能在run函数中建立,如果在UI界面上有个点击发送数据的按钮的话,是无法将按钮点击事件传递到run()函数中的,显然,此种方法不合适。

(1)主线程调用子线程内QSerialPort的发送数据函数发数据
如果想点击按钮发送数据的话,可以这么做:主线程点击按钮后emit一个信号,将这个信号与子线程中的槽连接,在槽函数中调用QSerialPort的发送数据函数。

(2)主线程显示子线程内QSerialPort收到的数据
同样的,如果想将子线程里QSerialPort接收到的数据显示到UI界面上,可以在子线程QSerialPort收到数据后emit一个信号出来,将该信号和主线程里的槽函数连接,就实现了将数据传入到了主线程里,然后将数据显示到界面上就可以了。

tips:其实严格意义上来讲,Qt的多线程就不是使用继承QThread这种方式,而是moveToThread方式。

二、多线程使用的场景

通常小的程序,没那么复杂的程序其实不需要使用多线程也能满足的,因为现在的计算机性能都很高了,差不多也够用了。但是在很多场景中我们还是得需要使用多线程的。

1、处理耗时的程序的时候,或者需要等待、循环等操作的时候。

2、在处理计算量比较大的代码的时候。

以上两种如果不采用多线程的方式的话,会造成界面卡顿的现象,因为Qt默认都是在一个主线程中的,如果要提高程序的响应就必须要这样做。这一点要特别的留意。

3、像一些C/S架构的软件,使用多线程并发去响应用户的请求。

4、对于多核的cpu来说,还可以提高利用率。

Qt中多线程要注意的事项
其实,计算机中并不是线程用的越多越好,因为多线程会涉及到线程安全的问题,这个问题本文就不分享了。另外多线程的本质是时间片段的切换,只不过cpu处理的快,像是同时在处理。就算是多核的计算机也会涉及到状态切换等问题,这些管理都是需要消耗资源的。

所以在Qt的编程中,如果不是迫不得已的情况下,一般的我们不建议线程数多于3个。

同时,我也不建议把串口接收的类封装为一个多线程的操作,因为串口和网络这些收发数据都是异步的,操作系统会调度,完全没必要再去封装为一个多线程。把接收到的数据需要计算的,耗时处理的扔到另一个线程里,这才是我们应该考虑的事情。

但是,有时候,我们迫不得已在串口或者网口接收数据后立马做一些操作,所以也会采用多线程的串口类。

在此,再次建议大家,不要把串口、网口这些接收的类封装到多线程中。

三、代码实现

封装一个类的串口接收类。同时、TCP/UDP接收的类都可以这样封装。

头文件如下:

Class SerialRender : public QObject
{
       Q_OBJECT
public:
     explicit SerialRender( QObject *parent = 0 );
     ~SerialRender();
 
     void setSerialPort( QSerialPort* serialPort );
     void setPortName( QString portName );
     void setBaudRate(int baudRate);
     void setInitData();
public slots:
     void openCommSlot(bool bOpenFlag);
     void readSerialPortData();
 
private:
     QSerialPort* m_serialPort = NULL;
     QThread * m_thread;
     QString m_portName;
     int m_baudRate;
};

cpp文件如下:

m_portName = portName ;
}
 
void SerialRender::setBaudRate(int baudRate)
{
    m_baudRate = baudRate;
}
 
void SerialRender::setInitData()
{
    m_thread = new QThread();
    this->moveToThread(m_thread);
    m_thread->start();
    connect( m_serialPort , SIGNAL( readyRead() ), this, SLOT(readSerialPortData())  );
}
 
void SerialRender::openCommSlot(bool bOpenFlag)
{  
    if ( bOpenFlag ){
        m_serialPort->flush();
        m_serialPort->setPortName(m_portName);
        m_serialPort->open(QIODevice::ReadWrite);
        m_serialPort->setBaudRate(m_baudRate );
        m_serialPort->setDataBits(QSerialPort::Data8);
        m_serialPort->setParity(QSerialPort::NoParity);
        m_serialPort->setStopBits(QSerialPort::OneStop);
        if (m_serialPort->isOpen()){
             qDebug()<<"打开成功";
        }else{
             qDebug()<<"打开失败";
        }
    }else{
        m_serialPort->flush();
        m_serialPort->close();
    }
}
 
void SerialRender::readSerialPortData()
{
    QByteArray data = m_serialPort->readAll();
 
    /***do something***/
}

第二种方式
qserialwork.h

#ifndef QSERIALWORK_H
#define QSERIALWORK_H

#include <QObject>
#include <QSerialPort>

#define QSERIALWORK_ERR_OK               0
#define QSERIALWORK_ERR_OPEN_SUCCESS     1
#define QSERIALWORK_ERR_OPEN_FAILED      2

class QSerialWork : public QObject
{
    Q_OBJECT
public:
    explicit QSerialWork(QObject *parent = nullptr);

    ~QSerialWork();

    void InitSerialPort();

    bool isOpen();

public slots:

    void OpenPort(QString portName);

    void ClosePort();

    void WriteData(const QByteArray& buf, qint64 len);

private slots:

    void DataArrived();

signals:

    void NewData(QByteArray data);

    void State(int err);

private:

    QSerialPort* m_serialPort;

    bool m_isOpen;

};

#endif // QSERIALWORK_H

qserialwork.cpp

#include "qserialwork.h"
#include <QCoreApplication>
#include <QDataStream>
#include <QDateTime>
#include <QDir>
#include <QThread>
#include <QtDebug>

QSerialWork::QSerialWork(QObject *parent) : QObject(parent)
{
    qDebug() << __FUNCTION__ << "Thread ID:" << QThread::currentThreadId();

    m_isOpen = false; 
}

QSerialWork::~QSerialWork()
{
    qDebug() << __FUNCTION__ << "Thread ID:" << QThread::currentThreadId();
}

void QSerialWork::InitSerialPort()
{
    qDebug() << __FUNCTION__ << "Thread ID:" << QThread::currentThreadId();

    m_serialPort = new QSerialPort(this);
    connect(m_serialPort, &QSerialPort::readyRead, this, &QSerialWork::DataArrived);
}

bool QSerialWork::isOpen()
{
    return m_isOpen;
}

void QSerialWork::OpenPort(QString portName)
{
    qDebug() << __FUNCTION__ << "Thread ID:" << QThread::currentThreadId();

    m_serialPort->setPortName(portName);
    m_serialPort->setBaudRate(QSerialPort::Baud115200);
    m_serialPort->setDataBits(QSerialPort::Data8);
    m_serialPort->setParity(QSerialPort::NoParity);
    m_serialPort->setStopBits(QSerialPort::OneStop);
    m_serialPort->setFlowControl(QSerialPort::NoFlowControl);

    m_isOpen = m_serialPort->open(QSerialPort::ReadWrite);

    if(m_isOpen)
    {
        emit State(QSERIALWORK_ERR_OPEN_SUCCESS);
    }else
    {
        emit State(QSERIALWORK_ERR_OPEN_FAILED);
    }
}

void QSerialWork::ClosePort()
{
    qDebug() << __FUNCTION__ << "Thread ID:" << QThread::currentThreadId();

    m_serialPort->close();
}

void QSerialWork::WriteData(const QByteArray& buf, qint64 len)
{
    qDebug() << __FUNCTION__ << "Thread ID:" << QThread::currentThreadId();

    m_serialPort->write(buf, len);
//    QString str = "fb200580ff020a00f8010000100000000100e71345040000fd0d0ffe";
//    QByteArray send_buf = QByteArray::fromHex(str.toLatin1());
//    emit NewData(send_buf);
}

void QSerialWork::DataArrived()
{
    qDebug() << __FUNCTION__ << "Thread ID:" << QThread::currentThreadId();

    QByteArray data = m_serialPort->readAll();

    emit NewData(data);
}