一篇又臭又长的流水账,要看结论可以直接拉到最后。
在一个项目中,需要使用串口接收外部的对射管状态,然后调用传感器。由于在之前的项目中,自制了一个带有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. 要一个成员完全处于子线程中进行处理,则只能通过信号与其进行交互。
2. QT的预处理是真的有魔法
这个带有UI的串口管理类,在完善后会开源源代码。