上篇文章中简单介绍了如何使用 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;
}
构造函数中,主要做了如下步骤:
- 首先创建工作者对象,并与线程相关联。
- 连接控制器的 startCalcSum 信号和工作者的 doWork 槽函数,即发送 startCalcSum 信号时,触发 doWork 槽函数。这里要说明的是,因为是跨线程的信号和槽的链接,这里默认的链接方式是使用 队列连接 。具体信号和槽的链接方式可参考 Qt中的信号和槽 。
- 连接工作者的 doFinished 信号和控制器的 onCalcSumFinished 函数。当计算完成时,会触发 doFinished 信号,同样这里也是 队列连接 的方式。
- 连接线程的 finished 信号和工作者的 deleteLater 槽函数。当线程中不再有事件被执行并且事件循环停止退出的时候,QThread发送该 finished 信号。
- 调用函数 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
整体流程如下:
- 函数 startThreadRunFunc 发送信号 startCalcSum ,此过程在主线程中执行。
- 触发 doWork 槽函数,计算前100个数的和,并发送信号 doFinished 信号,此过程在新建的线程中执行。
- 触发 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() 函数,循环执行
- 函数 isInterruptionRequested() 默认值为false,放调用函数 requestInterruption() 函数时,isInterruptionRequested() 的返回值为true,且这两个函数都是线程安全的。
- 通过变量 m_startThread 判断是否需要执行计算,这是一个 std::atomic<bool> 类型的变量,为原子量,为了保证共享内容的线程安全。
- 计算并发送信号 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
分析:
- 调用函数 calcSum 函数,在新线程中计算前100个数的和并发送信号 doFinished 。
- 主线程接收信号,并执行槽函数 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.feijiblog.com
文章同步页(文章末尾可下载代码): http://www.feijiblog.com/blog/threadqt