使用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() 的对象及其父对象必须在同一个线程中。


作者:douzhq
个人主页:http://www.douzhq.cn
文章同步页(文章末尾可下载代码): http://www.douzhq.cn/thread_qthread/

发布了88 篇原创文章 · 获赞 79 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/douzhq/article/details/104156580