使用Qt中的QThread创建线程

上篇文章中简单介绍了如何使用 Windows API 和c++11中的 std::thread 创建线程。
线程的创建和基本使用
本篇文章将会介绍如何使用QThread创建线程。

  • QThread是Qt所有线程控制的基础,每一个QThread实例对象控制一个线程。
  • QThread可以直接实例化使用也可以用继承的方式使用,QThread以事件循环的方式,允许继承自QObject的槽函数在线程中被调用执行。子类化QThread可以在开启线程事件循环之前初始化一个新线程,或者不使用事件循环的方式执行并行代码。

1. 使用信号和槽的形式触发

QThread的入口执行函数是 run() 函数,默认 run() 函数会通过调用函数 exec() 开启事件循环在线程中。可以使用函数 QObject::moveToThread()将一个工作对象与线程对象相关联。
下面是一个简单的示例,示例中在一个新线程中计算前n个数的和后通过信号返回给调用者:
工作类头文件, Worker.h

#ifndef WORKER_H
#define WORKER_H

#include <QObject>
class Worker : public QObject
{
    Q_OBJECT

public:
    Worker(QObject* parent = nullptr);
    ~Worker();

public slots:
    // 计算前count个数的和
    void doWork(int count);

signals:
    // 发送计算完成信号
    void doFinished(int);
};

#endif

工作类CPP文件, Worker.cpp

#include "Worker.h"
#include <QDebug>
#include <QThread>

Worker::Worker(QObject* parent)
    :QObject(parent)
{

}

Worker::~Worker()
{

}

// 计算 0~count个数的和
void Worker::doWork(int count)
{
    int sum = 0;
    for (int i=0; i<=count; ++i)
        sum += i;

    // 打印当前函数名,线程ID,以及计算结果
    qDebug() << __FUNCTION__ << "Thread ID: " << QThread::currentThreadId() << ", Result is " << sum;

    emit doFinished(sum);
}

槽函数 void doWork(int count); 用来计算前count个数的和,计算完成后,发送信号 doFinished(int) 其中的参数是计算结果。这就是一个工作类,与线程一点关系没有。

接下来是控制器
控制器头文件,Controller.h

#ifndef MYOBJECT_H
#define MYOBJECT_H

#include <QObject>
#include <QThread>

class Controller : public QObject
{
    Q_OBJECT

public:
    Controller(QObject* parent = nullptr);
    ~Controller();

    // 开启线程计算
    void startThreadRunFunc(int number);

private:
    QThread m_thread;

signals:
    // 该信号用于触发工作者中的槽函数
    void startCalcSum(int);

private slots:
    // 接受计算完毕后的结果槽函数
    void onCalcSumFinished(int sum);
};

#endif

控制器CPP文件,Controller.cpp

#include "Controller.h"
#include "Worker.h"
#include <QDebug>

Controller::Controller(QObject* parent)
    :QObject (parent)
{
    // [1]
    Worker* worker = new Worker;
    worker->moveToThread(&m_thread);

    // [2]
    QObject::connect(this, &Controller::startCalcSum, worker, &Worker::doWork);
    // [3]
    QObject::connect(worker, &Worker::doFinished, this, &Controller::onCalcSumFinished);

    // [4] 当线程退出时,释放工作者内存
    QObject::connect(&m_thread, &QThread::finished, worker, &Worker::deleteLater);

    // [5]
    m_thread.start();
}

Controller::~Controller()
{
    m_thread.quit();
    m_thread.wait();
}

void Controller::startThreadRunFunc(int number)
{
    // 发送开始计算信号
    emit startCalcSum(number);
    qDebug() << __FUNCTION__ << " : Current Thread is " << QThread::currentThreadId();
}

void Controller::onCalcSumFinished(int sum)
{
    // 打印行数名,当前线程ID,计算结果
    qDebug() << __FUNCTION__ \
             << " : Current Thread is " << QThread::currentThreadId() \
             << ", Result is " << sum;
}

构造函数中,主要做了如下步骤:

  1. 首先创建工作者对象,并与线程相关联。
  2. 连接控制器的 startCalcSum 信号和工作者的 doWork 槽函数, 即发送 startCalcSum 信号时,触发 doWork 槽函数。 这里要说明的是,因为是跨线程的信号和槽的链接,这里默认的链接方式是使用 队列连接 。 具体信号和槽的链接方式可参考 Qt中的信号和槽
  3. 连接工作者的 doFinished 信号和控制器的 onCalcSumFinished 函数。 当计算完成时,会触发 doFinished 信号,同样这里也是 队列连接 的方式。
  4. 连接线程的 finished 信号和工作者的 deleteLater 槽函数。 当线程中不再有事件被执行并且事件循环停止退出的时候,QThread发送该 finished 信号。
  5. 调用函数 start()开启线程。

main函数中代码如下:

#include <QCoreApplication>
#include <QThread>
#include "Controller.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    // 创建控制器
    Controller *object = new Controller;
    // 计算前100个数的和
    object->startThreadRunFunc(100);

    return a.exec();
}

执行结果如下
Controller::startThreadRunFunc : Current Thread is 0x491c
Worker::doWork Thread ID: 0x62c0 , Result is 5050
Controller::onCalcSumFinished : Current Thread is 0x491c , Result is 5050

整体流程如下:

  1. 函数 startThreadRunFunc 发送信号 startCalcSum ,此过程在主线程中执行。
  2. 触发 doWork 槽函数,计算前100个数的和,并发送信号 doFinished 信号,此过程在新建的线程中执行。
  3. 触发 onCalcSumFinished 槽函数,此过程在主线程中执行。

2. 使用继承自QThread方式触发

在Qt4.x的时候,QThread的常用方式是继承QThread重载函数 run()run() 函数是新线程的入口函数。我们同样完成上功能,代码如下:
CThread头文件:

#ifndef CTHREAD_H
#define CTHREAD_H

#include <QThread>
#include <atomic>
class CThread : public QThread
{
    Q_OBJECT

public:
    CThread(QObject* parent = nullptr);
    ~CThread();

    // 线程入口函数
    void run(void) override;

    // 计算前 0 ~ number的和
    void calcSum(int number);

private:
    std::atomic<bool> m_startThread;
    std::atomic<int> m_number;

signals:
    // 发送计算完成信号
    void doFinished(int);

private slots:
    // 相应计算完成结果
    void onDoFinished(int sum);
};

#endif

CThread源文件

#include "CThread.h"
#include <QDebug>

CThread::CThread(QObject* parent)
    :QThread (parent)
    ,m_startThread(false)
    ,m_number(0)
{
    QObject::connect(this, &CThread::doFinished, this, &CThread::onDoFinished);
    this->start();
}

CThread::~CThread()
{
    this->requestInterruption();
    this->wait();
}

void CThread::run(void)
{
    while (!this->isInterruptionRequested())
    {
        // 判断是否开启线程计算
        if (!m_startThread)
        {
            QThread::msleep(20);
            continue;
        }

        // 计算 0 ~ m_number的和
        int number = m_number;
        int sum = 0;
        for (int i = 0; i<=number; ++i)
            sum += i;

        // 打印函数名,线程ID,结果
        qDebug() << __FUNCTION__ \
                 << " : Current Thread Id is " << QThread::currentThreadId() \
                 << ", Result is " << sum;

        m_startThread = false;

        // 发送信号
        emit doFinished(sum);
    }
}

// 计算前 0 ~ number的和
void CThread::calcSum(int number)
{
    m_number = number;
    m_startThread = true;
}

void CThread::onDoFinished(int sum)
{
    // 打印函数名,线程ID,结果
    qDebug() << __FUNCTION__ \
             << " : Current Thread Id is " << QThread::currentThreadId() \
             << ", Result is " << sum;
}

run() 函数,循环执行

  1. 函数 isInterruptionRequested() 默认值为false, 放调用函数 requestInterruption() 函数时, isInterruptionRequested()的返回值为true,且这两个函数都是线程安全的。
  2. 通过变量 m_startThread 判断是否需要执行计算,这是一个 std::atomic<bool> 类型的变量,为原子量,为了保证共享内容的线程安全。
  3. 计算并发送信号 doFinished()

调用部分如下:

#include <QCoreApplication>
#include "CThread.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    // 使用继承QThread的方式开启线程计算前100个数的和
    CThread *thread = new CThread;
    thread->calcSum(100);

    return a.exec();
}

结果如下:
CThread::run : Current Thread Id is 0x68c0 , Result is 5050 CThread::onDoFinished : Current Thread Id is 0x5ce0 , Result is 5050

分析:

  1. 调用函数 calcSum 函数,在新线程中计算前100个数的和并发送信号 doFinished
  2. 主线程接收信号,并执行槽函数 onDoFinished

3. 几点说明

关于QThread我个人认知的一点点说明:

(1)使用信号和槽的方式是Qt的推荐方式,有两点好处:
  • 可以分离线程和具体实现,比如工作者对象可以在单独的线程中执行,也可以在主线程中执行。
  • 可以有多个线程的函数入口,创建多少个槽函数就有多少个线程函数入口。
(2)关于线程的等待退出

信号和槽的方式,使用如下代码:

m_thread.quit();
m_thread.wait();

quit() 函数会退出事件循环,wait() 函数阻塞等待线程退出。

继承QThread的方式,使用如下代码:

this->requestInterruption();
this->wait();

当使用 isInterruptionRequested()run() 函数作为循环条件时,可以先请求退出,然后再阻塞等待线程的退出。

(3) 线程对象和线程是两个不同的概念。比如上面的例子
CThread *thread = new CThread;

thread 对象就是一个线程对象,该对象的归属是主线程。因此该线程对象的槽函数的执行是在主线程中的; 使用函数 moveToThread() 是更改对象的归属线程,因此信号和槽的方式触发函数的执行是在新线程中。值得注意的是,线程中实现槽函数的触发,必须需要执行事件循环即 exec() 函数。

(4)GUI的相关操作只能在主线程中完成。

QWidget等对象的创建和操作必须在主线程中完成,其他的非界面相关的类可以在不同的线程中操作。 moveToThread() 的对象及其父对象必须在同一个线程中。

不会飞的纸飞机
扫一扫二维码,了解我的更多动态。

下一篇文章:使用Windows API实现自定义线程类CThread