【QT】QThread源码浅析
本章会挑出QThread源码中部分重点代码来说明QThread启动到结束的过程是怎么调度的。其次因为到了Qt4.4版本,Qt的多线程就有所变化,所以本章会以Qt4.0.1和Qt5.6.2版本的源码来进行浅析。
一、QThread类的定义源码
Qt4.0.1版本源码:
#ifndef QT_NO_THREAD
class Q_CORE_EXPORT QThread : public QObject
{
public:
...//省略
explicit QThread(QObject *parent = 0);
~QThread();
...//省略
void exit(int retcode = 0);
public slots:
void start(QThread::Priority = InheritPriority); //启动线程函数
void terminate(); //强制退出线程函数
void quit(); //线程退出函数
...//省略
signals:
void started(); //线程启动信号
void finished(); //线程结束信号
...//省略
protected:
virtual void run() = 0;
int exec();
...//省略
};
#else // QT_NO_THREAD
Qt5.6.2版本源码:
#ifndef QT_NO_THREAD
class Q_CORE_EXPORT QThread : public QObject
{
Q_OBJECT
public:
...//省略
explicit QThread(QObject *parent = Q_NULLPTR);
~QThread();
...//省略
void exit(int retcode = 0); //线程退出函数
...//省略
public Q_SLOTS:
void start(Priority = InheritPriority); //启动线程函数
void terminate(); //强制退出线程函数
void quit(); //线程退出函数
...//省略
Q_SIGNALS:
void started(QPrivateSignal); //线程启动信号
void finished(QPrivateSignal); //线程结束信号
protected:
virtual void run();
int exec();
...//省略
};
#else // QT_NO_THREAD
从以上两个版本的代码可以看出,这些函数在声明上基本没什么差异,但是仔细看,两个版本的 run() 函数声明的是不是不一样?
- Qt4.0.1版本run() 函数是纯虚函数,即此类为抽象类不可以创建实例,只可以创建指向该类的指针,也就是说如果你需要使用QThread来实现多线程,就必须实现QThread的派生类并且实现 run() 函数;
- Qt5.6.2版本的run() 函数是虚函数,继承QThread类时,可以重新实现 run() 函数,也可以不实现。
注:我查看了多个Qt版本的源码,发现出现以上差异的版本是从Qt4.4开始的。从Qt4.4版本开始,QThread类就不再是抽象类了。
二、QThread::start()源码
再来看看QThread::start()源码,Qt4.0.1版本和Qt5.6.2版本此部分的源码大同小异,所以以Qt5.6.2版本的源码为主,如下:
void QThread::start(Priority priority)
{
Q_D(QThread);
QMutexLocker locker(&d->mutex);
if (d->isInFinish) {
locker.unlock();
wait();
locker.relock();
}
if (d->running)
return;
... ... // 此部分是d指针配置
#ifndef Q_OS_WINRT
... ... // 此部分为注释
d->handle = (Qt::HANDLE) _beginthreadex(NULL, d->stackSize, QThreadPrivate::start,
this, CREATE_SUSPENDED, &(d->id));
#else // !Q_OS_WINRT
d->handle = (Qt::HANDLE) CreateThread(NULL, d->stackSize, (LPTHREAD_START_ROUTINE)QThreadPrivate::start,
this, CREATE_SUSPENDED, reinterpret_cast<LPDWORD>(&d->id));
#endif // Q_OS_WINRT
if (!d->handle) {
qErrnoWarning(errno, "QThread::start: Failed to create thread");
d->running = false;
d->finished = true;
return;
}
int prio;
d->priority = priority;
switch (d->priority) {
... ... // 此部分为线程优先级配置
case InheritPriority:
default:
prio = GetThreadPriority(GetCurrentThread());
break;
}
if (!SetThreadPriority(d->handle, prio)) {
qErrnoWarning("QThread::start: Failed to set thread priority");
}
if (ResumeThread(d->handle) == (DWORD) -1) {
qErrnoWarning("QThread::start: Failed to resume new thread");
}
}
挑出里面的重点来说明:
(1)Q_D()宏定义
在看源码的时候,当时比较好奇start函数的第一条语句 Q_D()宏定义 是什么意思,所以就看了下源码,在此也顺便讲讲,Q_D() 源码是一个宏定义,如下:
#define Q_D(Class) Class##Private * const d = d_func()
此处利用了预处理宏里的 ## 操作符:连接前后两个符号,变成一个新的符号。将Q_D(QThread)展开后,变成:QThreadPrivate * const d = d_func()。
(2)_beginthreadex()函数
上面d->handle = (Qt::HANDLE) _beginthreadex ( NULL, d->stackSize, QThreadPrivate::start, this, CREATE_SUSPENDED, &( d->id ) ) 语句中的函数是创建线程的函数,其原型以及各参数的说明如下:
unsigned long _beginthreadex(
void *security, // 安全属性,NULL为默认安全属性
unsigned stack_size, // 指定线程堆栈的大小。如果为0,则线程堆栈大小和创建它的线程的相同。一般用0
unsigned ( __stdcall *start_address )( void * ),
// 指定线程函数的地址,也就是线程调用执行的函数地址(用函数名称即可,函数名称就表示地址)
void *arglist, // 传递给线程的参数的指针,可以通过传入对象的指针,在线程函数中再转化为对应类的指针
//如果传入this,这个this表示调用QThread::start的对象地址,也就是QThread或者其派生类对象本身
unsigned initflag, // 线程初始状态,0:立即运行;CREATE_SUSPEND:suspended(悬挂)
unsigned *thrdaddr // 用于记录线程ID的地址
);
三、QThreadPrivate::start()源码
从QThread::start()源码可以知道,QThreadPrivate::start是重点,其实际就是调用了QThreadPrivate::start(this),这个 this 表示调用QThread::start的对象地址,也就是QThread或者其派生类对象本身。因为两个Qt版本此部分的源码大同小异,所以本部分主要是以5.6.2版本的源码为主,其源码以及说明如下:
// 参数arg就是上面所说的this
unsigned int __stdcall QT_ENSURE_STACK_ALIGNED_FOR_SSE QThreadPrivate::start(void *arg)
{
QThread *thr = reinterpret_cast<QThread *>(arg);
QThreadData *data = QThreadData::get2(thr);
// 创建线程局部存储变量,存放线程id
qt_create_tls();
TlsSetValue(qt_current_thread_data_tls_index, data);
data->threadId = reinterpret_cast<Qt::HANDLE>(quintptr(GetCurrentThreadId()));
QThread::setTerminationEnabled(false);
{
QMutexLocker locker(&thr->d_func()->mutex);
data->quitNow = thr->d_func()->exited;
}
if (data->eventDispatcher.load()) // custom event dispatcher set?
data->eventDispatcher.load()->startingUp();
else
createEventDispatcher(data);
...//省略
emit thr->started(QThread::QPrivateSignal()); // 发射线程启动信号
QThread::setTerminationEnabled(true);
thr->run(); // 调用QThread::run()函数 -- 线程函数
finish(arg); //结束线程
return 0;
}
由上述源码可以看出,实际上 run() 函数是在这里调用的,并且发出了 started() 启动信号,等到 run() 函数执行完毕,最后是调用了 QThreadPrivate::finish 函数结束线程,并且在finish内会发出 QThread::finished() 线程已结束的信号。
四、QThread::run()源码
再看看QThread::run()函数的源码。在上面 《2.1 QThread类的定义源码》的小节,我们可以看到两个Qt版本声明此方法的方式不一样,Qt-4.0版本将此定义为了纯虚函数,而Qt-5.6版本将此定义为了虚函数,那我们就看看Qt-5.6版本中,QThread::run()是如何定义的,如下:
void QThread::run()
{
(void) exec();
}
每一个 Qt 应用程序至少有一个 事件循环 ,就是调用了 QCoreApplication::exec() 的那个事件循环。不过,QThread也可以开启事件循环。只不过这是一个受限于线程内部的事件循环。因此我们将处于调用main()函数的那个线程,并且由 QCoreApplication::exec() 创建开启的那个事件循环成为 主事件循环 ,或者直接叫 主循环 。注意,QCoreApplication::exec()只能在调用main()函数的线程调用。主循环所在的线程就是主线程,也被成为 GUI 线程,因为所有有关 GUI 的操作都必须在这个线程进行。QThread的局部事件循环则可以通过在 QThread::run() 中调用 QThread::exec() 开启。
我们通过以上源码可以看到,它的定义很简单,就是调用了一个函数:QThread::exec() 开启线程中的 事件循环 ,我们也可以通过继承QThread,重写run()函数的方式,让其实现相对复杂的逻辑代码。如果你的线程需要将某些槽函数在本线程完成的话,就必须开启事件循环,否则在线程内无法响应各种信号并作出相应的行为。
小结: 比Qt-4.4版本更早的版本中,我们使用QThread启动线程时,就必须要实现继承于QThread的派生类,并且一定要重写run函数,若需要使用事件循环,就需要在run函数中添加exec()。到了Qt4.4版本之后(包括Qt4.4版本),QThread就不是抽象类了,不派生也可以实例化,在不重写QThread::run()方法,start启动线程是默认启动事件循环的。
注:当程序跑到了exec()代码时,位于exec()后面的代码就不会再被执行,除非我们使用quit、exit等退出语句来退出事件循环,退出后,程序才会继续执行位于exec()后面的代码。
五、QThread::quit()、QThread::exit()、QThread::terminate()源码
线程停止函数的区别,从Qt源码来分析:
(1)QThread::quit()、QThread::exit()
//QThread::quit()声明
void quit();
//QThread::quit()定义
void QThread::quit()
{ exit(); }
//QThread::exit()声明
void exit(int retcode = 0);
//QThread::exit()定义
void QThread::exit(int returnCode)
{
Q_D(QThread);
QMutexLocker locker(&d->mutex);
d->exited = true;
d->returnCode = returnCode;
d->data->quitNow = true;
for (int i = 0; i < d->data->eventLoops.size(); ++i) {
QEventLoop *eventLoop = d->data->eventLoops.at(i);
eventLoop->exit(returnCode);
}
}
由以上源码可知,QThread::quit() 和 QThread::exit(0) 的调用是等效的,都是告诉线程的事件循环,以返回码0(成功)退出。如果线程没有事件,则此函数不执行任何操作,也就是无效的。当线程拥有事件循环并且正处于 事件循环(QThread::exec()) 的状态时,调用 QThread::quit()或者QThread::exit() 线程就会马上停止,否则不会立刻停止线程,直到线程处于事件循环也就是正在执行 QThread::exec() 时,才会停止线程。
如果重复调用 QThread::quit()或者QThread::exit() 会有什么影响吗?
重复调用 QThread::quit()或者QThread::exit() 也不会有什么影响,因为只有拥有事件循环的线程,这两个函数才会生效停止线程的功能。
(2)QThread::terminate()
void QThread::terminate()
{
Q_D(QThread);
QMutexLocker locker(&d->mutex);
if (!d->running)
return;
if (!d->terminationEnabled) {
d->terminatePending = true;
return;
}
// Calling ExitThread() in setTerminationEnabled is all we can do on WinRT
#ifndef Q_OS_WINRT
TerminateThread(d->handle, 0);
#endif
QThreadPrivate::finish(this, false); //结束线程函数
}
在这个函数定义的最后一个语句,是调用了 QThreadPrivate::finish(this, false); 函数,其函数作用是直接退出线程,无论线程是否开启了事件循环都会生效,会马上终止一个线程,但这个函数存在非常不安定因素,不推荐使用。
如果重复调用 QThread::terminate() 会有什么影响吗?
没有影响。我们可以看到函数体里面的第三条语句,它首先会判断线程是否还在运行中,如果不是,会直接退出函数,就不会继续往下执行调用QThreadPrivate::finish(this, false); 函数了。
六、章节小结
相信看了以上的一些QThread源码,都大概知道了QThread类的本质以及QThread开启到结束的过程。这里我再简单总结下:
(1)QThread的本质:
- QThread 是用来管理线程的,它所依附的线程和它管理的线程并不是同一个东西;
- QThread 所依附的线程,就是执行 QThread t 或 QThread * t=new QThread 所在的线程;
- QThread 管理的线程,就是 run 启动的线程,也就是次线程。
(2)在这里针对Qt4.4版本之后(包括Qt4.4版本)简单汇总一下线程启动到结束的过程:
- QThread对象或者QThread派生类对象显式调用QThread类中的外部start()方法;
- QThread::start()方法再调用QThreadPrivate::start()方法;
- 在QThreadPrivate::start()方法内调用了QThread::run()虚函数,对使用者来说到了这里才是真正进入了一个新的线程里面。也就是说定义QThread对象或者QThread派生类对象的时候,还是在原来的线程里面,只有进入run函数才是进入了新的线程;
- 在QThreadPrivate::start()方法调用QThread::run()虚函数结束后,就会继续调用QThreadPrivate::finish()函数来结束线程,并发出线程结束的信号finished()。
(3)QThread::quit()、QThread::exit()、QThread::terminate():
- 对线程重复使用这三个停止线程的函数,没有任何影响;
- 尽量不要使用QThread::terminate()停止线程,此方式是强制退出线程,没有安全保障。
- 调用QThread::quit()和QThread::exit()一样。
(4)Qt各版本QThread类的变化:
- Qt4.4版本之前QThread类是属于抽象类, Qt4.4版本之后(包括4.4版本)不是抽象类。
【QT】QThread源码浅析的更多相关文章
- 【QT】 QThread部分源码浅析
本文章挑出QThread源码中部分重点代码来说明QThread启动到结束的过程是怎么调度的.其次因为到了Qt4.4版本,Qt的多线程就有所变化,所以本章会以Qt4.0.1和Qt5.6.2版本的源码来进 ...
- 【深入浅出jQuery】源码浅析--整体架构
最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...
- 【深入浅出jQuery】源码浅析2--奇技淫巧
最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...
- Struts2源码浅析-ConfigurationProvider
ConfigurationProvider接口 主要完成struts配置文件 加载 注册过程 ConfigurationProvider接口定义 public interface Configurat ...
- (转)【深入浅出jQuery】源码浅析2--奇技淫巧
[深入浅出jQuery]源码浅析2--奇技淫巧 http://www.cnblogs.com/coco1s/p/5303041.html
- HashSet其实就那么一回事儿之源码浅析
上篇文章<HashMap其实就那么一回事儿之源码浅析>介绍了hashMap, 本次将带大家看看HashSet, HashSet其实就是基于HashMap实现, 因此,熟悉了HashMap ...
- Android 手势识别类 ( 三 ) GestureDetector 源码浅析
前言:上 篇介绍了提供手势绘制的视图平台GestureOverlayView,但是在视图平台上绘制出的手势,是需要存储以及在必要的利用时加载取出手势.所 以,用户绘制出的一个完整的手势是需要一定的代码 ...
- Android开发之Theme、Style探索及源码浅析
1 背景 前段时间群里有伙伴问到了关于Android开发中Theme与Style的问题,当然,这类东西在网上随便一搜一大把模板,所以关于怎么用的问题我想这里也就不做太多的说明了,我们这里把重点放在理解 ...
- 【深入浅出jQuery】源码浅析2--使用技巧
最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...
随机推荐
- 初识HTTP 图解 形象生动
使用软件: 通信猫 请自行下载 使用平台: win7 360浏览器 当前PC连接的是无线网,所以第2步查看的是 无线局域网适配器上的IP地址
- P5091 【模板】扩展欧拉定理
题目链接 昨天考试考到了欧拉公式,结果发现自己不会,就来恶补一下. 欧拉公式 \(a^b \bmod p = a^{b}\) \(b < \varphi(p)\) \(a^b \bmod p = ...
- 用 Java 实现的八种常用排序算法
八种排序算法可以按照如图分类 交换排序 所谓交换,就是序列中任意两个元素进行比较,根据比较结果来交换各自在序列中的位置,以此达到排序的目的. 1. 冒泡排序 冒泡排序是一种简单的交换排序算法,以升序排 ...
- 微信小程序实时将less编译为wxss
1.npm或者yarn全局安装wxss-cli npm install -g wxss-cli 2.运行waxes-cli命令(mp_wx为小程序目录) wxss ./mp_wx 实时监听mp_wx目 ...
- Informatic 内些坑
1. 工作流调用工作流(可实现无规则时间点自由调度) pmcmd startworkflow -sv 集成服务名称 -d 配置域名称 -u Administrator -p Administrato ...
- MeteoInfoLab脚本示例:SeaWiFS HDF Grid数据
SeaWiFS HDF Grid数据读取,特别是涉及到了文件的众多属性数据的读取,数据取对数后绘图.脚本程序: #Add data file f = addfile('D:/Temp/hdf/S199 ...
- 扫描仪扫描文件处理-ABBYY自动矫正图像歪斜
修改界面语言: 设置为不识别图片文字(因为我们只需要把图片歪斜校正): 保存无损彩色格式:
- kafka-伪集群搭建
一.简介 Apache Kafka是一个快速.可扩展的.高吞吐的.可容错的分布式"发布-订阅"消息系统,使用Scala与Java语言编写,能够将消息从一个端点传递到另一个端点, ...
- 运行shell文件时提示/bin/bash^M: bad interpreter: 没有那个文件
查看脚本文件是dos格式还是unix格式的几种办法.(1)cat -A filename 从显示结果可以判断,dos格式的文件行尾为^M$,unix格式的文件行尾为$:(2)od -t x1 file ...
- nginx集群:nginx配置负载均衡集群(nginx1.18.0)
一,nginx的负载均衡集群的特点: 1,nginx集群和lvs的不同? lvs集群:工作在第4层(传输层) nginx集群:工作在第7层(应用层) lvs集群:性能更强 nginx集群:功能更强:可 ...