集中梳理一下简单的 Qt 线程案例。
不管怎么看,Qt 框架对于线程的封装和Java倒是挺像的.
并发这里主要讲线程,但是线程这一块儿本身内容就很多,这里不讲QRunnable, QConcurrent等。
只介绍GUI编程最常用的,深入的东西,请参考文档。
线程简介
哪些地方用到了线程?
避免耗时操作导致主程序无响应,所以开辟新县城处理耗时任务。
耗时任务:
下面这段代码,如果在主线程,即 GUI 线程执行,那么肯定会造成界面卡死:
1 2 3 4 5 6 7 8 9 10
| if (m_Timer->isActive() == false) { m_Timer->start(100); }
QThread::sleep(5);
m_Timer->stop(); qDebug() << "over";
|
线程详解
QT4 线程
这个比较简单,依托于继承体系, 这就是你要去单独写一个线程类,在里面完成任务。
核心逻辑用下来这个图讲解了:
这种方式非常像 java 的其中一种线程。
归纳起来就是:
- run方法就是线程执行体
- 启动线程需要这样的一个线程实例(QThread子类实例, 主线程保留,因为还要为其收尸)
- 启动线程不能调用run, 而必须使用 start 方法 (start 方法可以指定优先级)
- run 方法里面发送信号,通知相关绑定的方法,任务已经完成
- 主线程收尸 (join之类的)
这就是常见的逻辑。
给个例子, 核心逻辑大概如下:
(线程的 run 方法定义和上面说的逻辑差不多)
工作线程类的实现(run方法):
之后绑定任务:
dealDone
槽函数是在 GUI 线程定义的,而信号是在非GUI线程定义和发送的,所以connect绑定的时候,应该指定队列的方式绑定。
这里采用的 auto 方式,即默认方式,自动就按照多线程的逻辑处理好了,所以不用担心。
计时器
因为计时器实在主线程启动的,所以这里计时器也只能在主线程内工作,即触发计时器绑定的槽函数。
Qt4计时器案例
这里写了一个案例, LCD数字显示器的计时,大致思路和上面差不多,直接贴代码了。
线程类设计如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #include <QThread>
class MyThread : public QThread { Q_OBJECT public: explicit MyThread(QObject *parent = nullptr);
protected: void run();
signals: void isDone();
public slots: };
void MyThread::run() { QThread::sleep(5);
emit isDone(); }
|
工作界面类如下: (主线程,主界面类)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| public: void dealTimer(); void dealDone();
private slots: void on_pushButton_clicked();
private: Ui::Widget *ui; QTimer *timer;
int count;
MyThread *thread;
#include "widget.h" #include "ui_widget.h" #include <QDebug> #include <QThread> //for sleep()
Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) { ui->setupUi(this);
setWindowTitle("耗时任务案例");
timer = new QTimer(this); connect(timer, &QTimer::timeout, this, &Widget::dealTimer);
thread = new MyThread(this); connect(thread, &MyThread::isDone, this, &Widget::dealDone); }
void Widget::on_pushButton_clicked() { count = 0; if (!timer->isActive()) { timer->start(50); }
qDebug() << "开启新线程任务"; thread->start();
}
void Widget::dealTimer() { count++; ui->lcdNumber->display(count); }
void Widget::dealDone() { qDebug() << "线程任务完毕了,关闭计时器, 结束线程" ; timer->stop(); thread->quit();
thread->wait(); }
Widget::~Widget() { delete ui; }
|
运行效果大致如下:
Qt5 线程
上面一种原始的线程方式,虽然简单,但是灵活。
其实我们原始的初衷是,我只是想让这部分代码或者这个耗时操作单独开一个线程执行,即让一个方法跑在非GUI线程而已。
Qt线程提供了 Java 中类似 Runnable 的方式, 非常灵活。
一般实现和使用的思路大致如下:
线程类可以是一个随便的类(一般叫做 worker 类),只要 moveToThread()即可。
为什么开启子线程后需要用信号和槽的方式去启动线程执行体,而不是直接启动?
- 直接 start 就相当于 GUI 线程直接调用这个方法(虽然把那个对象放入了线程中)
- 通过槽启动,其实是为了利用槽函数的 sender 和 receiver不再一个线程,可以用自动连接的方式在另外一个线程运行那个执行体.
最后注意两点:
- 千万注意,调用线程执行体,一定要用信号和槽去调用(主线程发信号,槽函数绑定为线程执行体)
- 自定义类Worker用于线程执行体的实例,就不能指定父对象,因为后面要 worker.moveToThread(QThread *); 所以不能给他定义主线程对象去管理它的声明周期
Qt5计时器案例
同样的计时处理,这次用 这种灵活的方式来处里,核心代码如下:(灵活的代价是,代码量显著增加了)
woker类的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| class MyThread : public QObject { private: bool isStop; public:
void myTimeout(); void setFlag(bool flag = true);
signals: void myTimeoutSignal(); };
MyThread::MyThread(QObject *parent) : QObject(parent) { isStop = false; }
void MyThread::myTimeout() { qDebug() << "子线程号: " << QThread::currentThread();
while(!isStop) { QThread::msleep(100); emit myTimeoutSignal(); } }
void MyThread::setFlag(bool flag) { isStop = flag; }
|
主线程的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
| class Widget : public QWidget { Q_OBJECT
public: explicit Widget(QWidget *parent = 0); ~Widget();
void dealTimeoutSignal();
signals: void startThreadInstance();
private slots: void on_btnStart_clicked(); void on_btnStop_clicked();
private: Ui::Widget *ui;
MyThread *threadInstance; QThread *thread;
int count; };
Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) { ui->setupUi(this);
threadInstance = new MyThread;
thread = new QThread(this); threadInstance->moveToThread(thread);
connect(threadInstance, &MyThread::myTimeoutSignal, this, &Widget::dealTimeoutSignal); qDebug() << "主线程号: " << QThread::currentThread();
connect(this, &Widget::startThreadInstance, threadInstance, &MyThread::myTimeout);
connect(this, &Widget::destroyed, this, [=](){ if (thread->isRunning()) { threadInstance->setFlag(true); thread->quit(); thread->wait();
delete threadInstance;
qDebug() << "终止子线程"; } }); }
Widget::~Widget() { delete ui; }
void Widget::dealTimeoutSignal() { count++; ui->lcdNumber->display(count); }
void Widget::on_btnStart_clicked() { if (thread->isRunning()) { qDebug() << "已经运行啦,还点它做啥?"; return; }
count = 0; thread->start(); threadInstance->setFlag(false); emit startThreadInstance(); qDebug() << "开启子线程";
}
void Widget::on_btnStop_clicked() { if (!thread->isRunning()) { qDebug() << "都没有在跑, 停止啥?"; return; } threadInstance->setFlag(true); thread->quit(); thread->wait(); qDebug() << "终止子线程"; }
|
貌似并没有第一种方式简单,其实并不是,核心逻辑还是在不同线程的两个对象,通过信号槽启动线程执行体。你可以先不管这个线程执行体在干啥,其他它也简单,它要让LCD Number的数字改变,但是他不能操作UI,所以他只能再发信号给主线程,让主线程变一下UI。
核心逻辑其实很简单。
运行截图:
线程综合案例
以前在总结网络的时候,已经说了一个 QNetworkManager 同步下载案例,那个案例里面下载操作就是通过线程实现的网络文件读写。这里说另外一种耗时操作,UI绘制
。
线程函数不允许操作图形界面,那么怎么绘制?
确实,子线程不能操作图形界面,但这仅仅意味着不能再UI控件上显示图形&绘制推行,但是它可以做图形的数据处理,IO读写。
如果图形是从文件里读取的或者根据具体的平台模拟生成的,这个时候如果放在主线程里面做这个耗时操作,显然会造成界面假死。
那么可以把这些图形读取,或者图形数据的生成等任务交给子线程,然后传递给主线程就可以了。
下面这个案例,就借助 QImage 这个图像载体实现跨线程的图形传输任务,主要思路如下:
主线程任务: 开启子线程(并调用绘图线程体),接收子线程发来的图片
子线程的任务: 绘图,发送图片主线程(借助信号发送)
期间都是通过信号和槽机制实现的。
具体的代码,大致如下:
worker类的定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| class Worker : public QObject { Q_OBJECT public: explicit Worker(QObject *parent = nullptr);
void run();
signals: void sendImage(QImage image); };
void Worker::run() { QImage image(300, 300 , QImage::Format_ARGB32); QPainter p(&image);
QPen pen; pen.setWidth(5); p.setPen(pen);
QBrush brush; brush.setStyle(Qt::CrossPattern); brush.setColor(Qt::green); p.setBrush(brush);
qsrand(QTime::currentTime().second());
QPoint a[] = { QPoint(qrand()%300, qrand()%300), QPoint(qrand()%300, qrand()%300), QPoint(qrand()%300, qrand()%300), QPoint(qrand()%300, qrand()%300), QPoint(qrand()%300, qrand()%300), };
p.drawPolygon(a, 5);
emit sendImage(image); }
|
主线程核心代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| public: void recvImage(QImage);
protected: void paintEvent(QPaintEvent *);
private: Ui::Widget *ui;
Worker *worker; QThread *thread; QImage image;
Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) { ui->setupUi(this);
worker = new Worker; thread = new QThread(this); worker->moveToThread(thread);
thread->start();
connect(ui->pushButton, &QPushButton::pressed, worker, &Worker::run);
connect(worker, &Worker::sendImage, this, &Widget::recvImage);
connect(this, &Widget::destroyed, [=](){ if (thread->isRunning()) { thread->quit(); thread->wait(); } delete worker; }); }
Widget::~Widget() { delete ui; }
void Widget::recvImage(QImage image) { this->image = image; repaint(); }
void Widget::paintEvent(QPaintEvent *) { QPainter p(this); p.drawImage(50, 50, image); }
|
运行的效果图如下:
(总是无法覆盖第一次的图形,可能是因为绘图器上必须至少要有一个图形)