0. QT中线程启动的方式
0.1 继承QThread
继承QThread时,子类必须重写run方法,保证线程在手动结束之前持续运行。
当子类使用start方法启动后,run方法会在生命周期内循环执行。
.h文件
#ifndef CLASS1_H
#define CLASS1_H
#include <QObject>
#include <QThread>
// 继承QTQThread
class Class1 : public QThread
{
Q_OBJECT
public:
explicit Class1(QThread *parent = 0);
~Class1();
void run() override;
public slots:
void testFunc();
};
#endif // CLASS1_H
.cpp文件
#include "class1.h"
#include <QDebug>
Class1::Class1(QThread *parent) : QThread(parent)
{
}
Class1::~Class1()
{
}
void Class1::run()
{
qDebug() << "class 1 run threadID : " << currentThreadId();
while(1){ // 测试代码,写成了死循环,实际中应该避免这样写
msleep(5);
}
}
void Class1::testFunc()
{
qDebug() << "class1 testFunc threadID : " << currentThreadId();
}
0.2 使用moveToThread启动
这种方式较为简单,正常写自己的类即可,为了保证其方法可以在线程上执行,需要继承QObject
类。
在使用时首先生成子类对象,同时new一个QThread出来,然后使用子类的moveToThread
方法,将子类放到刚刚new出来的QThread上,再启动该线程即可。
.h文件
#ifndef CLASS2_H
#define CLASS2_H
#include <QObject>
// 使用moveToThread
class Class2 : public QObject
{
Q_OBJECT
public:
explicit Class2(QObject *parent = nullptr);
~Class2();
public slots:
void testFunc();
};
#endif // CLASS2_H
.cpp文件
#include "class2.h"
#include <QDebug>
#include <QThread>
Class2::Class2(QObject *parent) : QObject(parent)
{
}
Class2::~Class2()
{
}
void Class2::testFunc()
{
qDebug() << "class2 testFunc threadID : " << QThread::currentThreadId();
}
0.3 使用QtConcurrent启动
QtConcurrent方式通常用于在当前线程中启动一个耗时的方法,将该方法单独放在一个线程上执行。只需要一行代码即可。
启动后,当前线程不会阻塞,继续向下执行。可以配合QFuture类获取异步执行的结果。
1. 对象方法调用时的坑
1.1 对象方法调用方式
有两种。
- 直接调用,
object.func()
或者object->func
- 使用QT中的信号槽方式,
1.2 坑
使用继承QThread
的方式时,只有run方法和run调用的方法会在子线程上执行。
- 当主线程
直接
调用该类的某个方法时,该方法由主线程负责执行。 - 当主线程使用信号槽的方式调用该类的某个方法时,该方法由
主线程
负责执行。
使用moveToThread
的方式时,具有更好的灵活性。
- 当主线程
直接
调用该类的某个方法时,该方法由主线程负责执行。 - 当主线程使用信号槽的方式调用该类的某个方法时,由主线程执行还是由子线程执行,
根据信号槽的连接方式确定
。
2. 信号槽的5种连接方式
在Qt的信号槽机制当中,有5种连接方式。
在代码中使用信号槽机制的时候,通常只需要下边的一行代码
connect(sender, signal, receiver, slot)
connect
方法除了信号发送者、信号、信号接收者、槽函数之外,还有第五个参数:这对信号槽的连接方式。
2.1 直接连接
Qt::DirectConnection
槽函数在信号发送者所在的线程中执行,效果就像是在信号发射位置直接调用槽函数。
2.2 队列连接
Qt::QueuedConnection
槽函数在信号接收者所在线程中执行,但是,该槽函数不会立即被执行
,是放入到信号接收者线程中的槽函数队列内,等到该槽函数前边的任务都执行完毕,才会执行该槽函数。
可见,如果槽函数接收者线程中有死循环槽函数被执行的话,那么在死循环之后的槽函数就永远都不会被执行。
使用这种方式时,信号发送者信号发出后,当前线程不会阻塞,会继续向下执行。
2.3 阻塞队列连接
Qt::BlockingQueuedConnection
与队列连接类似,但:在使用阻塞队列连接方式,发送信号后,当前线程会进入到阻塞状态,直到槽函数执行完毕才恢复。
可见:使用这种方式时,如果信号和槽函数在同一个线程中,会造成死锁。
2.4 自动连接
一般情况下,或者刚接触Qt开发时,都是使用的这种方式进行连接。
在自动连接方式中:
- 如果信号发送者和接收者在同一个线程中,则使用直接连接
Qt::DirectConnection
- 如果信号发送者和接收者在不同线程中,则使用队列连接
Qt::QueuedConnection
2.5 单一连接
Qt::UniqueConnection
其实,这个不算连接方式,该值可以确保当前信号和当前槽函数的连接仅进行一次,不可以进行重复的相同连接。
可以通过或
运算,与其他4种方式结合使用。
3. 测试代码
创建Qt工程,创建界面如下:
在ui类中添加class1和class2两个类的指针(上边有代码),class1使用继承QThread
的方式,class2使用moveToThread
的方式。
界面中的4个按钮都是调用class1和class2对象中的testFunc
方法输出对象方法的执行线程ID。但调用方式不同。
TestClass1和TestClass2按钮直接调用,其余两个使用信号槽的方式调用。
代码如下:
.h 文件
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QDebug>
#include <QThread>
#include <QTime>
#include "class1.h"
#include "class2.h"
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow();
signals:
void signal_test1_func();
void signal_test2_func();
private slots:
void on_pushButton_test1_clicked();
void on_pushButton_test1s_clicked();
void on_pushButton_test2_clicked();
void on_pushButton_test2s_clicked();
private:
Ui::MainWindow *ui;
Class1 *m_c1Thread = nullptr; // 继承QThread的
Class2 *m_c2 = nullptr; // 使用moveToThread
QThread *m_c2Thread = nullptr;
};
#endif // MAINWINDOW_H
.cpp 文件
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
qDebug() << "main threadID : " << QThread::currentThreadId();
m_c1Thread = new Class1;
m_c2 = new Class2;
m_c2Thread = new QThread(this);
m_c2->moveToThread(m_c2Thread);
connect(this, &MainWindow::signal_test1_func, m_c1Thread, &Class1::testFunc);
connect(this, &MainWindow::signal_test2_func, m_c2, &Class2::testFunc); // 这里的连接方式一会要做修改
m_c1Thread->start();
m_c2Thread->start();
}
MainWindow::~MainWindow()
{
m_c1Thread->quit();
m_c1Thread->wait();
m_c2Thread->quit();
m_c2Thread->wait();
delete m_c1Thread;
delete m_c2Thread;
delete m_c2;
delete ui;
}
void MainWindow::on_pushButton_test1_clicked()
{
// m_c1Thread 继承自 QThread
m_c1Thread->testFunc();
}
void MainWindow::on_pushButton_test1s_clicked()
{
// m_c1Thread 继承自 QThread
emit signal_test1_func();
}
void MainWindow::on_pushButton_test2_clicked()
{
// m_c2 使用 moveToThread 启动
m_c2->testFunc();
}
void MainWindow::on_pushButton_test2s_clicked()
{
// m_c2 使用 moveToThread 启动
emit signal_test2_func();
}
注意代码中的
connect(this, &MainWindow::signal_test2_func, m_c2, &Class2::testFunc);
这里,class2对象的连接方式使用默认的方式,即自动连接
。
程序运行后,按第一行、第二行的顺序点击4个按钮,输出结果如下:
仅有第四个按钮(使用moveToThread启动线程,信号槽方式调用对象方法)在子线程上执行。可见,class2对象的连接方式此时是队列连接
。
当修改class2对象的连接方式为队列
connect(this, &MainWindow::signal_test2_func, m_c2, &Class2::testFunc, Qt::QueuedConnection);
再次执行,得到结果如下:
四个按钮的方法都在主线程上执行了,印证了1.2节中的描述。