一、前言
之前在学Qt的的时候,老是搞不清楚Qt什么时候做耗时操作会导致UI阻塞什么时候又不会。本文通过记录学Qt时的一个小实验来探索Qt背后的核心机制---事件循环。
本文代码仓库为ricardo/qt槽函数线程实验,具体代码版本为ricardo/qt槽函数线程实验 - Gitee.com,如果我的代码对您学习Qt、完成课程作业等有些许帮助的话,还请对该仓库点一个star,祝您生活愉快。
如果本文被CSDN自动设为VIP可见,请从我的CSDN个人简介去到我的Gitee主页寻找代码。
二、实验设置
本实验大致代码与前文[C++ ]qt槽函数及其线程机制-CSDN博客的代码大致上一样,但有新的改变。
上文中主要是在两个槽函数中向一个多线程同步队列中分别push奇数偶数元素,来探索qt槽函数的线程机制。而本文在此基础上,将上文中两个槽函数的内容(耗时操作)放到多线程里面去实现,同时通过一个进度条来实时显示队列中占用空间的百分比。
2.1 定时刷新主界面ui实现思路
同步队列是在主界面类中定义的,由于其size设置得比较大(2000000),原本槽函数中就是会压入2000000个元素,所以每push一个元素就更新一次ui界面肯定是不太合理的。由于人眼对实时性的要求是24HZ,所以只要保证界面刷新速率在24HZ及以上就OK,完全不需要去"真正的实时"根据每时每刻的变化来刷新界面。
到这里设计思路就很明显啦,只需要一个计时器每隔42ms触发一个ui刷新的信号或者函数就可以啦。
cpp
// .h中MainWindow界面类中定义
QTimer* timer;
// .cpp中MainWindow界面类中初始化
// 精确设置24Hz:1000ms / 24 ≈ 42ms
// 使用Qt::PreciseTimer提高精度
timer = new QTimer(this);
timer->setTimerType(Qt::PreciseTimer);
timer->start(42); // 42ms间隔
利用上面的函数就可以实现定时触发计时器信号即可完成ui定时刷新。
cpp
connect(timer, &QTimer::timeout,this, &MainWindow::onRefreshTimerTimeout);
2.2 多线程中运行耗时工作
在上一篇文章的实验中,主线程的槽函数中运行这非常耗时的操作,这会导致ui界面非常卡顿,为此做出的改进思路如下:
cpp
//定义一个Worker类继承QObject,内含一个function对象,接收不同的耗时函数操作
//在MainWindow中定义若干Worker类对象和QThread对象,使用moveToThread,将worker对象所处线程移入到QThread线程中。
worker1 = new Worker();
worker2 = new Worker();
worker3 = new Worker();
thread1 = new QThread(this);
thread2 = new QThread(this);
thread3 = new QThread(this);
worker1->moveToThread(thread1);
worker2->moveToThread(thread2);
worker3->moveToThread(thread3);
worker1->setFunc([&](){
qDebug() << "开始压入数据";
for(int i = 0; i < 200000; i += 2){
sq.push(i);
qDebug() << i;
// if(timer1.elapsed() > 100){
// ui->progressBar->setValue(static_cast<int>(sq.length()*100/capacity));
// timer1.restart();
// }
}
});
worker2->setFunc([&](){
for(int i = 1; i < 200000; i += 2){
sq.push(i);
qDebug() << i;
// if(timer2.elapsed() > 100){
// ui->progressBar->setValue(static_cast<int>(sq.length()*100/capacity));
// timer2.restart();
// }
}
});
worker3->setFunc([&](){
// sq.clear(); 该代码会通过一个lock_guard锁住然后进行长时间的while循环 pop元素,会导致ui卡主
//利用下面的代码替代上面的,避免ui卡主
while(!sq.empty()){
sq.try_pop();
}
});
thread1->start();
thread2->start();
thread3->start();
这种方式是QT官方比较推荐的多线程编程方式。
三、原理探究---事件循环
根据前文中QT ui主线程机制的探索,本文的QTimer是在主界面类的主线程运行的,设置的是42ms提醒一次,在Qt主线程里面使用QTimer这种计时的操作,会不会导致阻塞。
答案是不会!
QTimer实际工作原理如下:
- timer->start() 只是向事件循环注册一个定时器事件
- 主线程继续执行,不会停在这里
- Qt底层会管理定时器
- 每过42ms,Qt向事件队列投递一个QTimerEvent
- 主线程空闲时处理这个事件,触发timeout()信号
而主线程事件循环机制可以用如下伪代码表示:
cpp
// Qt主线程的核心循环(简化版)
while (app.isRunning()) {
// 1. 处理所有待处理的事件(鼠标、键盘、定时器、网络等)
while (event = getNextEvent()) {
processEvent(event); // 调用你的onRefreshTimerTimeout()
}
// 2. 如果没有事件,等待(不占用CPU)
waitForEvents(); // 进入休眠,直到有新事件
// 3. 重复
}
在一个时间线上,有可能是这样发生的
时间(t+0ms): start() 调用,立即返回
时间(t+10ms): 用户点击按钮,处理点击事件
时间(t+20ms): 处理网络数据
时间(t+42ms): Qt投递定时器事件
时间(t+45ms): 事件循环空闲,处理timeout信号 → 调用你的槽函数
时间(t+50ms): 继续处理其他事件
这里的定时器事件就与主线程UI,更新画面的绘制事件一样,都是事件,通过事件循环机制来在主线程中轮询处理。
当然在上述的时间线中同样有导致ui卡顿的可能。可以这样理解,把上述时间线看成若干可变长的时间片,每一个时间片会去完成一个"操作",比如" 用户点击按钮,处理点击事件","Qt投递定时器事件"," 事件循环空闲,处理timeout信号 → 调用你的槽函数"这三个操作就对应三个时间片。
在" 事件循环空闲,处理timeout信号 → 调用你的槽函数"中,如果调用的槽函数非常耗时,那么这个时间片就会无限增长知道该操作处理完成。而UI画面更新操作update()也是在事件循环里面的,假如其前面就有很长的时间片在处理其他操作,就会导致update()不能及时执行。这也是为什么Qt的主线程不能执行耗时操作的原因。
一句话总结:Qt的事件循环机制运行在主线程中,很多像QTimer这种异步操作都是在Qt事件循环机制中加入不耗时的事件操作等来实现的。