Qt多线程问题分析及解决思路QObject: Cannot create children for a parent that is in a different thread
Chapter1 Qt多线程问题分析及解决思路QObject: Cannot create children for a parent that is in a different thread
原文链接:https://blog.csdn.net/laoponline/article/details/118003801
一篇又臭又长的流水账,要看结论可以直接拉到最后。
在一个项目中,需要使用串口接收外部的对射管状态,然后调用传感器。由于在之前的项目中,自制了一个带有UI的串口管理类(继承QDialog)最早在主线程中生成这个串口管理类。但是发现程序变得越来越复杂以后,主线程会出现几十甚至几百毫秒的的连续占用,可能会导致串口响应不及时,状态刷新迟到,传感器采集不到完整的图像。
准备将带有UI的QSerialport通过Movetothread直接丢到子线程里运行,发现带有UI的类是不能这么干的…于是只能苦哈哈的重写这个串口管理类。
最早的想法是,将QSerialport直接Movetothread,发现可以编译通过,但是并没有卵用。仔细研究后发现,这个QSerialport是在串口管理类中直接声明的。由于串口管理类是在Mainwindow的初始化中new出来的,这个QSerialport自然也是在主线程中生成的。
class Serial_with_Dialog : public QDialog
{
Q_OBJECT
public:
explicit Serial_with_Dialog(QWidget *parent = nullptr, QString port_name = "", QPushButton* button = nullptr);
~Serial_with_Dialog();
QSerialPort serial_port;
....
于是改为只声明指针,在CPP中new一个QSerialPort。发现会提示QObject: Cannot create children for a parent that is in a different thread。此时程序虽然可以正常运行,但是对QSerialPort进行操作依旧是在主线程中进行。
头文件中:
class Serial_with_Dialog : public QDialog
{
Q_OBJECT
public:
explicit Serial_with_Dialog(QWidget *parent = nullptr, QString port_name = "", QPushButton* button = nullptr);
~Serial_with_Dialog();
QSerialPort* serial_port;
....
cpp文件中:
...
serial_port = new QSerialPort;
...
根据之前的研究经验,即使Movetothread后,如果直接调用函数,函数还是会在调用者的线程中运行。必须通过信号和槽进行QueuedConnection连接,才能通过槽让函数运行在子线程上。于是为QSerialport重新写了一个包含类Serial_Thread,继承了QObject,并将这个包含类movetothread。
头文件中:
class Serial_Thread : public QObject
{
Q_OBJECT
public:
explicit Serial_Thread(QWidget *parent = nullptr);
~Serial_Thread();
QSerialPort* serial_port;
....
class Serial_with_Dialog : public QDialog
{
Q_OBJECT
public:
explicit Serial_with_Dialog(QWidget *parent = nullptr, QString port_name = "", QPushButton* button = nullptr);
~Serial_with_Dialog();
Serial_Thread* serial_thread;
CPP文件中:
Serial_with_Dialog::Serial_with_Dialog(QWidget *parent, QString port_name, QPushButton *button) :
QDialog(parent) ,
ui(new Ui::Serial_with_Dialog)
{
ui->setupUi(this);
//Language_Change(); //切换语言
serial_thread = new Serial_Thread(nullptr);
serial_thread = new Serial_Thread(nullptr);
.....
这里在Serial_Thread中将QSerialPort* serial_port;暴露出来,主要是因为在Serial_with_Dialog中有许多直接调用QSerialPort进行串口开关、收发等操作的函数。为了减少对这些函数的修改,通过指针直接调用这个QSerialPort。经过一番修改,程序可以正常运行,但是在qdebug窗口中会出现错误提示:
QObject: Cannot create children for a parent that is in a different thread
字面意义是说,不能在一个线程中为不在同一个线程中的父类生成新的成员。由于程序可以正常运行,就没有在意。但是经过仔细的测试,发现虽然对这个QSerialPort进行读、写操作的函数,都在子线程中运行,但是实际上这个QSerialPort本身依旧是在主线程中进行处理的。通过while(1)卡死主线程后,这个QSerialPort就失去了响应。
经过网上一番搜索,也没有对应的方案。于是自己一点点思考,感觉可能是因为这个QSerialPort是在Serial_Thread的构造函数中生成的。因为构造函数是在主线程中运行的,这个QSerialPort必然也是在主线程中生成的,所以必然在主线程中处理。于是给Serial_Thread写了个Init函数,通过信号连接,等Serial_Thread所处的QThread运行start()以后再通过信号调用初始化。
void Serial_Thread::Init()
{
qDebug()<<tr("[Serial_Thread]Init, time= %1, @ %2").arg(clock()).arg(QString::number(quintptr(QThread::currentThreadId())));
serial_port = new QSerialPort(nullptr); //新建
QObject::connect(serial_port, &QSerialPort::readyRead, this, &Serial_Thread::Serial_Read,Qt::QueuedConnection); //连接读取函数
}
经过debug测试,这次Init确实是在子线程中运行了,但是QObject: Cannot create children for a parent that is in a different thread的提示依旧在,QSerialPort也依旧在主线程中运行。转了半天原来还在原地。
没有放弃,继续测试,在Serial_Thread中,其它的槽函数中另外new多个QSerialPort出来,发现只要不把这个QSerialPort的指针赋给serial_port,就不会出现前面的错误提示。甚至在Serial_Thread声明了一个无用的serial_port2,将new出来的QSerialPort赋值给它,也不会出现错误提示。
结合之前的实验仔细思考了一下,Serial_Thread是在主线程中生成的,属于Mainwindow。通过Movetothread到新线程中的QObject,自身应该是分裂(或者说复制)成了两分。原来的那一份还是在主线程,或者叫做父线程中运行,通过指针可以直接调用。在新线程中,生成了新的一份实体,其中跟父线程完全无关的部分成员,会在生成并且运行在子线程中,跟父线程有直接关联的部分,实际调用的还是在主线程中运行,一些成员变量则会保持同步。(纯猜想)
通过实验验证,在子线程中new一个QSerialPort,只要不将其指针赋值给serial_port,而是赋值给无用的serial_port2,就不会出现错误提示。一旦想要通过任何方式将其指针赋值给在串口管理类中会有调用的serial_port,还是会出现前的错误提示。QT应该是通过某种预处理上的的魔法操作,判定在Serial_Thread的所有者中,会直接调用serial_port,于是规定serial_port必须留在主线程。
看来是没得偷懒了,必须对程序结构进行大改动。删除原先串口管理类中所有对于QSerialPort的直接调用,全部改为通过信号和槽进行调用。改了一个小时,终于将serial_port变为了Serial_Thread的私有成员,所有操作都通过线程间的消息进行传递。
头文件中:
class Serial_Thread : public QObject
{
Q_OBJECT
public:
explicit Serial_Thread(QWidget *parent = nullptr);
~Serial_Thread();
bool isOpen() { return m_isOpen;}
int error() { return m_error;}
QString errorString() { return m_error_string;}
QSerialPort* port() {return serial_port;}
signals:
void signal_direct_incoming(QByteArray index,long sendtime = 0); //子线程直接发出的信号,不受主线程的影响
public slots:
void Slot_Send(QByteArray index); //发送字符串
void Slot_Clear(); //清空缓冲区
void Init();
void Slot_PortOpen(QSerialPortInfo info, int baudrate);
void Slot_PortClose();
void Slot_CheckError();
private slots:
void Serial_Read();
private:
QSerialPort* serial_port;
.....
class Serial_with_Dialog : public QDialog
{
Q_OBJECT
public:
explicit Serial_with_Dialog(QWidget *parent = nullptr, QString port_name = "", QPushButton* button = nullptr);
~Serial_with_Dialog();
Serial_Thread* serial_thread;
.....
signals:
void signal_Log_Add(QString index);
void signal_incoming(QByteArray index); //通过界面转发的数据。这里通过了一层转发,速度会受到主线程工作强度的影响
void signal_Init(); //在子线程中初始化
void signal_PortOpen(QSerialPortInfo info, int baudrate); //对子线程中的实体QSerialport进行操作
void signal_PortClose();
void signal_CherkError();
void signal_Clear();
void signal_Send(QByteArray index);
......
CPP文件中:
Serial_with_Dialog::Serial_with_Dialog(QWidget *parent, QString port_name, QPushButton *button) :
QDialog(parent) ,
ui(new Ui::Serial_with_Dialog)
{
ui->setupUi(this);
//Language_Change(); //切换语言
serial_thread = new Serial_Thread(nullptr);
QThread *thread_serial = new QThread(nullptr); //为保证实时性,移动到子线程中进行调用
serial_thread->moveToThread(thread_serial);
QObject::connect(thread_serial, &QThread::finished, this, &QObject::deleteLater); // 清理线程
thread_serial->start(); // 开启线程
QObject::connect(this, &Serial_with_Dialog::signal_Init, serial_thread, &Serial_Thread::Init,Qt::BlockingQueuedConnection); //连接发送函数
signal_Init();
QObject::connect(serial_thread, &Serial_Thread::signal_direct_incoming, this, &Serial_with_Dialog::Serial_Read,Qt::QueuedConnection); //连接读取函数
QObject::connect(this, &Serial_with_Dialog::signal_Send, serial_thread, &Serial_Thread::Slot_Send,Qt::QueuedConnection); //连接发送函数
QObject::connect(this, &Serial_with_Dialog::signal_Clear, serial_thread, &Serial_Thread::Slot_Clear,Qt::QueuedConnection); //连接发送函数
QObject::connect(this, &Serial_with_Dialog::signal_PortOpen, serial_thread, &Serial_Thread::Slot_PortOpen,Qt::BlockingQueuedConnection); //对串口进行操作的函数,需要等待操作结束
QObject::connect(this, &Serial_with_Dialog::signal_PortClose, serial_thread, &Serial_Thread::Slot_PortClose,Qt::BlockingQueuedConnection);
QObject::connect(this, &Serial_with_Dialog::signal_CherkError, serial_thread, &Serial_Thread::Slot_CheckError,Qt::BlockingQueuedConnection);
......
最终完成测试已经是两个小时以后了…这次终于达成了目的,初始化中不在出现前面的错误提示,QSerialPort完全在子线程中运行,即使主线程进入while(1)卡死,QSerialPort依旧可以正常响应外部输入的串口消息,并且与处于其他子线程中的类进行交互。
结论:
1. QObject: Cannot create children for a parent that is in a different thread 这个错误提示的实际意思是,不能在子线程中生成跨线程调用的成员。如果一个成员在父线程中被直接调用了,那么这个成员必须处在父线程中,强行在子线程中生成就会出现这个错误提示。
2. 要一个成员完全处于子线程中进行处理,则只能通过信号与其进行交互。
3. QT的预处理是真的有魔法
<根本原因分析及重要解决方法>Chapter2 关于QObject: Cannot create children for a parent that is in a different thread
网上找了半天资料,都看得云里雾里,直到看到这篇文章:https://blog.csdn.net/MYTCHITOS/article/details/105892321
作为一个qt初学者,免不了要踏入QObject: Cannot create children for a parent that is in a different thread这个坑,不论是官方文档、stackoverflow还是csdn,看了半天也摸不着头脑,只好老老实实研究错误代码,最终解决了问题,这里记录一下。
代码从这里开始,一些不同重要的内容就略去了。
Class Worker:public QObject
{
QTcpSocket tcpSocket;
public:
Worker();
};
Class Controller:public QObject
{
public:
Controller();
};
Controller::Controller()
{
Worker worker=new Worker;
QThread threads=new Qthreads;
worker->moveToThread(threads);
threads->start();
}
这个时候编译,其实也能通过,并且能够执行,但就是在执行的时候qt发出了
QObject: Cannot create children for a parent that is in a different thread
(Parent is QTcpSocket(0xdc3e80), parent's thread is QThread(0xcebf20), current thread is QThread(0xdc4160)
先不管这里什么意思,问题怎么解决呢?就是这样
Class Worker:public QObject
{
QTcpSocket *tcpSocket;
public:
Worker()
{
tcpSocket=new QTcpSocket(this);
}
};
这里的关键是在new tcpSocket对象时传入的this参数,这个参数表示tcpSocket这个对象的父对象是这个Worker的一个实例。
我们回头看,为什么这样就可以了。
首先要明确一点,这个错误提示是qt本身对线程处理机制带来的,不是C++本身的问题,也与通常的线程机制无关。
我们可以看到在Controller的构造函数中创建了一个Worker对象worker,以及一个QThread对象threads,然后把这两个对象连接起来。那么问题就是,在Qt看来,调用Controller构造函数的线程,与threads代表的线程是两个线程,而在构造worker时所顺便创建的QTcpSocket对象tcpSocket与worker本身最初同属于Controller所在的线程,worker执行moveToThread之后,worker就转移到了threads所代表的线程中,但是,要注意,tcpSocket仍然属于Controller所在的线程,于是在调用threads->start()的时候就会出现错误提示。
而在构造tcpSocket时如果明确指明其父对象,那么在将worker与threads挂钩时,就会自动的也把tcpSocket与threads挂接起来,于是问题解决。当然,这一点是我猜的,但不管怎么说,执行结果可以证明一切。
显然,如果不使用moveToThread而是重载run来实现线程,其实也是免不了会掉进这个坑,道理上面说了,处理的思路也是一样的。
Chapter3 自己编写的多线程双串口例程
serialworker.h文件
#ifndef SERIALWORKER_H
#define SERIALWORKER_H
#include <QObject>
#include <QSerialPort>
#include <QSerialPortInfo>
#include <QDebug>
#include <QByteArray>
#include <QThread>
struct serialParam {
};
class SerialWorker : public QObject
{
Q_OBJECT
public:
enum ErrorCodeFlag {
NoErr = 0x0000,
SerialPortOpenERR = 0x0001,
ReadOnly = 0x0001,
WriteOnly = 0x0002,
ReadWrite = ReadOnly | WriteOnly
};
Q_DECLARE_FLAGS(ErrorCode, ErrorCodeFlag)
public:
explicit SerialWorker(QObject *parent = nullptr);
explicit SerialWorker(QString portName, qint32 baudrate);
~SerialWorker();
QString ByteArrayToHexString(QByteArray data);
public slots:
/*****************************************
* 对应主线程的信号,需要的槽函数:
*1. 打开串口槽函数
*2. 关闭串口槽函数
*3. 修改参数槽函数
*****************************************/
void slot_openPort(QString portName, qint32 baudrate);
void slot_closePort();
void slot_handleMessage();
void slot_writeData(QByteArray data);
signals:
/*****************************************
*1. 通知主线程处理消息的信号
*2. 同时该信号需要传递哪些参数
*****************************************/
void sig_handleResults(const QString &result);
void sig_handleMsg(serialParam *para);
//void sig_Error(qint32 errCode); //比如打开串口失败等
void sig_Error(SerialWorker::ErrorCode); //比如打开串口失败等
private:
QSerialPort *serialPort1; //串口通信对象
serialParam *para1;
};
#endif // SERIALWORKER_H
serialworker.cpp文件
/*******************************************************************************
* 这里的关键是在new QSerialPort(this)对象时传入的this参数,这个参数表示serialPort1这个对象
* 的父对象时这个SerialWorker的一个实例。不加this就会导致QObject:Cannot create children for
* a parent that is in a different thread
******************************************************************************/
serialPort1 = new QSerialPort(this); //指定父对象为SerialWorker的一个实例
#include "serialworker.h"
SerialWorker::SerialWorker(QObject *parent) : QObject(parent)
{
}
SerialWorker::SerialWorker(QString portName, qint32 baudrate)
{
/*******************************************************************************
* 这里的关键是在new QSerialPort(this)对象时传入的this参数,这个参数表示serialPort1这个对象
* 的父对象时这个SerialWorker的一个实例。不加this就会导致QObject:Cannot create children for
* a parent that is in a different thread
******************************************************************************/
serialPort1 = new QSerialPort(this); //指定父对象为SerialWorker的一个实例
//设置端口名称和波特率
serialPort1->setPortName(portName);
serialPort1->setBaudRate(baudrate);
//关联读取信号与槽函数:处理信息
connect(serialPort1, &QSerialPort::readyRead, this, &SerialWorker::slot_handleMessage);
qDebug()<<"SerialWorker::SerialWorker threadId:"<<QThread::currentThreadId();
}
SerialWorker::~SerialWorker()
{
//申请的串口资源关闭,同时销毁,否则会导致内存泄漏
if(serialPort1->isOpen())
{
serialPort1->close();
}
serialPort1->deleteLater();
}
QString SerialWorker::ByteArrayToHexString(QByteArray data)
{
QString ret(data.toHex().toUpper());
int len = ret.length()/2;
qDebug()<<"收到字节长度为:"<<len;
for(int i=1;i<len;i++)
{
ret.insert(2*i+i-1," ");
}
return ret;
}
void SerialWorker::slot_openPort(QString portName, qint32 baudrate)
{
qDebug()<<"SerialWorker::slot_openPort threadId:"<<QThread::currentThreadId();
if(serialPort1->open(QIODevice::ReadWrite))
{
qDebug()<<"SerialWorker::slot_openPort:open successfully...";
}
else
{
qDebug()<<"SerialWorker::slot_openPort:open error ???";
emit sig_Error(SerialWorker::ErrorCodeFlag::SerialPortOpenERR);
}
}
void SerialWorker::slot_closePort()
{
if(serialPort1->isOpen())
{
serialPort1->close();
}
}
void SerialWorker::slot_handleMessage()
{
QByteArray buffer = serialPort1->readAll();
// 2.进行数据处理
QString result = ByteArrayToHexString(buffer);
qDebug()<<"SerialWorker::slot_handleMessage threadId:"<<QThread::currentThreadId();
qDebug()<<"子线程收到数据:"<<result;
// 3.将结果发送到主线程
emit sig_handleResults(result);
}
void SerialWorker::slot_writeData(QByteArray data)
{
if(serialPort1->isOpen())
{
serialPort1->write(data);
}
}
mainwindow.h文件
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include "serialworker.h"
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
QStringList findPort(void);
signals:
void sig_OpenSerialport(QString portName, qint32 baudrate);
void sig_serialWriteData(QByteArray data);
void sig_OpenSerialport2(QString portName, qint32 baudrate);
void sig_serialWriteData2(QByteArray data);
private slots:
void on_pBtn_OpenSerialPort_clicked();
void slot_errorCode(SerialWorker::ErrorCode);
void slot_handleResults(const QString &result);
void on_pBtn_SendData_clicked();
void on_pBtn_OpenSerialPort_2_clicked();
void slot_errorCode2(SerialWorker::ErrorCode);
void slot_handleResults2(const QString &result);
private:
Ui::MainWindow *ui;
SerialWorker* serialWork1; //创建一个串口工作线程实例指针
QThread threadSerial1; //串口工作线程
SerialWorker* serialWork2; //创建一个串口工作线程实例指针
QThread threadSerial2; //串口工作线程
};
#endif // MAINWINDOW_H
mainwindow.cpp文件
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
//搜寻添加串口
QStringList list = findPort();
ui->cmb_portName->addItems(list);
ui->cmb_portName->setCurrentIndex(1);
ui->cmb_portName_2->addItems(list);
//波特率
QStringList baudrateList;
baudrateList<<"4800"<<"9600"<<"19200"<<"38400"<<"57600"<<"115200";
ui->cmb_baudRate->addItems(baudrateList);//添加下拉列表选项
ui->cmb_baudRate->setCurrentText("115200");//界面中初始值
ui->cmb_baudRate_2->addItems(baudrateList);
ui->cmb_baudRate_2->setCurrentText("115200");
qDebug()<<"MainWindow::MainWindow threadId:"<<QThread::currentThreadId();
}
MainWindow::~MainWindow()
{
delete ui;
serialWork1->deleteLater();
threadSerial1.quit();
threadSerial1.deleteLater();
serialWork2->deleteLater();
threadSerial2.quit();
threadSerial2.deleteLater();
}
QStringList MainWindow::findPort()
{
QStringList list;
//通过QSerialPortInfo查找可用串口
foreach(const QSerialPortInfo &info, QSerialPortInfo::availablePorts())
{
QString showName = info.portName();
qDebug() << showName+":"+info.description();
list.append(showName);
}
return list;
}
void MainWindow::on_pBtn_OpenSerialPort_clicked()
{
serialWork1 = new SerialWorker(ui->cmb_portName->currentText().toLatin1().toStdString().data(), ui->cmb_baudRate->currentText().toInt());
serialWork1->moveToThread(&threadSerial1);
connect(serialWork1, &SerialWorker::sig_Error, this, &MainWindow::slot_errorCode);
connect(&threadSerial1, &QThread::finished, serialWork1, &QObject::deleteLater);
connect(serialWork1, &SerialWorker::sig_handleResults, this, &MainWindow::slot_handleResults); //从串口读取的数据发送给主线程
connect(this, &MainWindow::sig_OpenSerialport, serialWork1, &SerialWorker::slot_openPort);
connect(this, &MainWindow::sig_serialWriteData, serialWork1, &SerialWorker::slot_writeData); //从主线程发来的数据写入串口
threadSerial1.start();
emit sig_OpenSerialport(ui->cmb_portName->currentText(),ui->cmb_baudRate->currentText().toInt());
ui->pBtn_OpenSerialPort->setEnabled(false);
}
void MainWindow::slot_errorCode(SerialWorker::ErrorCode code)
{
qDebug() << "MainWindow::slot_errorCode:"<< code;
}
void MainWindow::slot_handleResults(const QString &result)
{
static quint32 cnt = 0;
ui->textEdit->append(result);
cnt++;
ui->lineEdit_Cnt->setText(QString::number(cnt));
// if(cnt > 50){
cnt = 0;
// ui->textEdit->clear();
// }
}
void MainWindow::on_pBtn_SendData_clicked()
{
QByteArray ba;
ba.append(ui->lineEdit_Send->text());
emit sig_serialWriteData(ba);
}
void MainWindow::on_pBtn_OpenSerialPort_2_clicked()
{
serialWork2 = new SerialWorker(ui->cmb_portName_2->currentText().toLatin1().toStdString().data(), ui->cmb_baudRate_2->currentText().toInt());
serialWork2->moveToThread(&threadSerial2);
connect(serialWork2, &SerialWorker::sig_Error, this, &MainWindow::slot_errorCode2);
connect(&threadSerial2, &QThread::finished, serialWork2, &QObject::deleteLater);
connect(serialWork2, &SerialWorker::sig_handleResults, this, &MainWindow::slot_handleResults2); //从串口读取的数据发送给主线程
connect(this, &MainWindow::sig_OpenSerialport, serialWork2, &SerialWorker::slot_openPort);
connect(this, &MainWindow::sig_serialWriteData, serialWork2, &SerialWorker::slot_writeData); //从主线程发来的数据写入串口
threadSerial2.start();
emit sig_OpenSerialport(ui->cmb_portName_2->currentText(),ui->cmb_baudRate_2->currentText().toInt());
ui->pBtn_OpenSerialPort_2->setEnabled(false);
}
void MainWindow::slot_errorCode2(SerialWorker::ErrorCode code)
{
qDebug() << "MainWindow::slot_errorCode2:"<< code;
}
void MainWindow::slot_handleResults2(const QString &result)
{
static quint32 cnt = 0;
ui->textEdit_2->append(result);
cnt++;
// if(cnt > 50){
cnt = 0;
// ui->textEdit_2->clear();
// }
ui->lineEdit_Cnt_2->setText(QString::number(cnt));
}