在独立的线程中使用QSerialPort引起关于QThread的使用探讨

这个故事说来话长,问题一个个的发生,我最终是否是真正的拨开了云雾我不知道,但是问题得到了很大程度上的解决。这些问题涉及到了Qt的串口和线程。

根源-一个令人苦恼的问题

在一个Qt编写的项目中,我遇到了一个令人苦恼,甚至发疯的问题。我有一个串口需要读写,而这个串口是通过一个开关发送信息给我的程序,程序到接到开关闭合的信息,会做出一系列动作,比如通过TCP链接另一台计算机,相互通信发送信息,当然还有其他一些动作要执行。这时候问题来了,当我快速的闭合和断开我的开关,关闭的消息经常收不到,也就是说QSerialPort的readReady链接的槽函数根本没有收到开关断开的数据,只是闭合的发送过来了。按照Qt的信号和槽的机制,就算我执行了一些操作在闭合和断开之间,但是我应该能收到断开的消息。当然,硬件没有问题,单片机的速度和稳定性没有问题,所以数据肯定是发过来了,但是Qt的串口模块没有发送这个数据过来。然而,当我取消闭合和断开之间的这些操作,只是单纯检测状态变化的话,不会出现快速闭合和断开收不到断开信号的问题。

思来想去,认为主线程的某些模块或者逻辑使得串口的信号出现了问题,虽然通过readReady信号绑定槽的方式接受串口数据是不会阻塞主线程的,但是我还是认为主线程的某些问题造成的。所以我决定将QSerialPort放到一个独立线程中发送和接受数据,然后通过信号和槽将这个线程和主线程保定数据的交互。

关于QThread的使用

搜索网上一些资料以及Qt的文档,很快我完成了将QSerialPort放进线程的工作,代码如下。

这个小的控制台的测试项目只有三个文件main.cpp, serialthread.h, serialthread.cpp,其中SerialThread类是一个QThread的子类。

serialthread.h的代码:

#ifndef SERIALTHREAD_H
#define SERIALTHREAD_H
 
#include <QThread>
#include <QMutex>
#include <QWaitCondition>
#include <QtSerialPort/QSerialPort>
#include <QDebug>
 
class SerialThread : public QThread
{
    Q_OBJECT
 
public:
    SerialThread(QObject *parent = 0);
    ~SerialThread();
 
    void transaction(const QString &portName);
    void run();
 
private slots:
    void readdata();
 
private:
    QString portName;
    QSerialPort *serial;
};
 
#endif // SERIALTHREAD_H

serialthread.cpp的代码:

#include "serialthread.h"
 
QT_USE_NAMESPACE
 
SerialThread::SerialThread(QObject *parent)
    : QThread(parent)
{
}
 
SerialThread::~SerialThread()
{
    if(isRunning()){
        quit();
    }
}
 
void SerialThread::transaction(const QString &portName)
{
    this->portName = portName;
 
    if (!isRunning())
        start();
}
 
void SerialThread::readdata()
{
    QByteArray baRead = serial->readAll();
    qDebug()<<"In SLOT,Recive data:"<<(QString(baRead));
    qDebug()<<"In SLOT,the thread ID:"<<currentThreadId();
    qDebug()<<"In SLOT,the thread ID of serial:"<<serial->thread()->currentThreadId();
}
 
void SerialThread::run()
{
    qDebug()<<"In run(),Serial Thread Start,thread ID:"<<currentThreadId();
 
    QString currentPortName;
    if (currentPortName != portName) {
        currentPortName = portName;
    }
 
    serial = new QSerialPort();
    qDebug()<<"In run(),function,the thread ID of serial:"<<serial->thread()->currentThreadId();
    serial->setPortName(currentPortName);
    connect(serial,SIGNAL(readyRead()),this,SLOT(readdata()));
 
    if (!serial->open(QIODevice::ReadWrite)) {
        qDebug()<<(tr("In run(),Can't open %1, error code %2")
                   .arg(portName).arg(serial->error()));
        return;
    }
    else{
        qDebug()<<"In run(),Open Serial success on "<<currentPortName;
    }
 
    exec();
}

main.cpp的代码:

#include <QCoreApplication>
#include "serialthread.h"
 
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
 
    SerialThread thread;
    thread.transaction("COM2");
 
    return a.exec();
}

运行结果如下:

其中字符串”hello”是我从串口发送过来测试用的。通过上面结果可以看到,run()函数运行的线程句柄是0x2c90,如果在run()函数,也就是这个新线程内获取对象serial的线程的话也是0x2c90,这个符合我的理解,因为虽然serial指针是在类里面定义的,但是我实在新的线程内实例化的。令我奇怪的是,当readRead信号触发,并且执行了我绑定的readdata槽函数后,输出的信息是这样的,在槽函数内线程的句柄是0x2fd4,这是主线程,也就是说,新建的线程内的对象所绑定的槽会在主线程内执行。但是,令我不解的是,在槽函数内也就是主线程内,获取serial的线程的时候,也是主线程。难道serial这个对象可以存在在两个线程内么,还是我理解的某个地方不对,暂时不管,后续解决。现在的主要问题是,这不是我想要的结果,串口读取信号所绑定的槽函数我认为必须在新建的线程内才是正常的,不然我想要解决的最初的问题也没办法测试了。所以serial对象和与他链接的任何槽函数都必须在一个独立线程内。

一个讨论已久的问题

于是我又像往常一样在网上搜索一些资料,于是看到了moveToThread函数,这个函数可以将某个对象移动到执行的线程内。看了很多人的用法是,在建立的QThread的子类的构造函数,比如这样:

class MyThread : public QThread
{
public:
    MyThread()
    {
        moveToThread(this);
    }
 
    void run();
 
signals:
    void progress(int);
    void dataReady(QByteArray);
 
public slots:
    void doWork();
    void timeoutHandler();
};

大家可以看看2010年一个讨论比较多的文章You’re doing it wrong,这个文章比较早了,但是博主Bradley T. Hughes所关注的这个问题却很好,可能由于当时Qt的关于QThread类的使用的文档还不是很详尽,所以他认为很多人都勿用了。按照文章所讲,QThread只是控制系统的线程的控制点,我们不应该过多的涉及我们的任务的问题,正确的方式是建立一个QObject的子类,这个子类主要涉及我们想要在线程中所要完成的工作,然后将这个子类的实例对象通过moveToThread添加到一个新的QThread的对象中。当然现在关于QThread的文档很详细的对其进行了说明,大家可以参照QThread Detailed Description,看到这里我明白了,于是有了另外一个测试项目。

一个我认为是正确的关于如何在新建线程中使用QSerialPort的例子

通过继承QObject,我建立一个叫做SerialWorker的类,这个类我将来把他添加到新的线程中,这个类里的所有槽函数和对象都将在新线程中工作。另外为了和SerialWorker进行信号和槽的交互,我建立一个MySerial的类,为了使用信号和槽同样继承了QObject类,在这个类里我实例化一个QThread,这将是我的串口工作的线程,另外实例化SerialWorker,然后将MySeriaal的一些信号和槽绑定到SerialWorker,因为信号和槽机制是线程安全的所以我不用考虑他们在不同的线程而造成问题。这样是因为我在控制台工程中进行测试,所以如果实在一个Widget的项目中,可以直接去使用SerialWorker类和QThread。贴一下代码:

serialworker.h

#ifndef SERIALWORKER_H
#define SERIALWORKER_H
 
#include <QObject>
#include <QtSerialPort/QSerialPort>
#include <QDebug>
#include <QThread>
 
class SerialWorker : public QObject
{
    Q_OBJECT
public:
    explicit SerialWorker(QObject *parent = nullptr);
 
signals:
    void serialReadReady(QByteArray data);
    void serialError(QString error);
 
public slots:
    void readdata();
    void senddata(QByteArray data);
    void init(QString portNmae);
 
private:
    QString portName;
    QString request;
    int waitTimeout;
    bool quit;
    bool m_bSendRequest;
    QSerialPort *serial;
};
 
#endif // SERIALWORKER_H

serialworker.cpp

#include "serialworker.h"
 
SerialWorker::SerialWorker(QObject *parent) : QObject(parent)
{
}
 
void SerialWorker::init(QString portNmae)
{
        qDebug()<<"SerialWorker, new thread ID:"<<this->thread()->currentThreadId();
    serial=new QSerialPort();
 
    serial->setPortName(portNmae);
 
    if(serial->open(QIODevice::ReadWrite)){
        connect(serial,SIGNAL(readyRead()),this,SLOT(readdata()));
        qDebug()<<"SerialWorker init, Open success on"<<portNmae;
    }
    else
        qDebug()<<"SerialWorker init, Open faild on"<<portNmae;
}
 
void SerialWorker::readdata()
{
    QByteArray baRead = serial->readAll();
    qDebug()<<"SerialWorker Slot, readdata,Recive:"<<QString(baRead);
    qDebug()<<"SerialWorker Slot, readdata threadid:"<<this->thread()->currentThreadId();
    qDebug()<<"SerialWorker Slot, object serial threadid:"<<serial->thread()->currentThreadId();
    emit this->serialReadReady(baRead);
}
void SerialWorker::senddata(QByteArray data)
{
 
}

myserial.h

#ifndef MYSERIAL_H
#define MYSERIAL_H
 
#include <QObject>
#include <QDebug>
#include <QThread>
#include "serialworker.h"
 
class MySerial : public QObject
{
    Q_OBJECT
public:
    explicit MySerial(QObject *parent = nullptr);
 
signals:
    //signals to thread
    void serialWorkerInit(QString portNmae);
    void serialWorkerSend(QByteArray data);
    void serialWorkerClose();
 
private slots:
    //slots from thread
    void readInfo(QByteArray baRead);
 
private:
    SerialWorker *_serialWorker;
    QThread *_workerThread;
    void init();
};
 
#endif // MYSERIAL_H

myserial.cpp

#include "myserial.h"
 
MySerial::MySerial(QObject *parent) : QObject(parent)
{
    _workerThread=new QThread();
    _serialWorker=new SerialWorker();
    _serialWorker->moveToThread(_workerThread);
 
    connect(this,SIGNAL(serialWorkerInit(QString)),_serialWorker,SLOT(init(QString)));
    connect(_workerThread, &QThread::finished, _serialWorker, &QObject::deleteLater);
    connect(_serialWorker,SIGNAL(serialReadReady(QByteArray)),this,SLOT(readInfo(QByteArray)));
    _workerThread->start();
    init();
}
 
void MySerial::init()
{
    emit serialWorkerInit("COM2");
}
 
void MySerial::readInfo(QByteArray baRead)
{
    qDebug()<<"MySerial Slot readInfo,Recive:"<<QString(baRead);
    qDebug()<<"MySerial Slot readInfo,threadid:"<<this->thread()->currentThreadId();
}

main.cpp

#include <QCoreApplication>
#include "myserial.h"
 
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
 
    qDebug()<<"main thread is:"<<a.thread()->currentThreadId();
    MySerial mySerial;
 
    return a.exec();
}

运行结果:

由运行结果可以看出,SerialWorker的所有槽都是运行在新建立的线程内。这是我想要的结果。

20170807更新:需要注意的是,MySerial类的构造函数在主线程内执行,同样的构造函数内的所有对象的构造函数也是在主线程内执行,我们的QSerialPort类必须在新建线程内通过new创建对象,所以我在SerialWorker.h定义的变量serial必须在某个成员函数中执行,不能再SerialWorker的构造函数中new,不然会出现readReady信号收不到的问题或其他某些问题。

最后的问题解决

我用这种方式,把我们项目中用到的串口模块用新建线程的方法进行了重写,最后快速闭合和断开开关造成的断开的信号收不到的问题得到了解决。但是其收不到的根本原因,我还不是很清楚,并且要想充分弄明白,需要一点点测试,并且需要写一些单独的程序去测试,而且最好不能用串口工具去模拟,必须用实际的单片机设备去调试。

分类: Qt | 标签:

发表评论

电子邮件地址不会被公开。 必填项已用*标注