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 对象方法调用方式

有两种。

  1. 直接调用,object.func()或者object->func
  2. 使用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节中的描述。