目录
1、QThread常用API
2、Qt线程安全
3、使用线程QThread
4、connect函数的第五个参数
5、Qt互斥锁
5.1 QMutexLocker
6、条件变量
7、信号量
结语
前言:
线程是应用程序开发非常重要的概念,在Qt中,用QThread类来实现多线程,将线程有关的各种函数都封装到这个类中(包括线程执行函数),方便通过该类对线程进行控制。若要创建一个线程执行某些任务,则需要自定义一个继承QThread的类,重写线程执行函数让该线程执行我们期望的任务。
1、QThread常用API
使用QThread创建一个线程对象后,需要调用相关API来操作线程,比如启动线程,等待线程等操作以及获取线程信息,这些成员函数的介绍如下:
run()
|
线程的执行函数,即线程所执行的任务,通过start函数间接调用他
|
start()
| 真正意义上的创建线程,调用此函数才能让线程执行run函数的内容 |
currentThread()
|
返回⼀个指向管理当前执行线程的QThread对象的指针
|
isRunning()
|
如果线程正在运行返回true,否则返回false
|
sleep() / msleep() / usleep()
|
使线程休眠,单位为秒 / 毫秒 / 微秒
|
wait()
| 线程等待,调用此函数的线程会阻塞在此函数处,若线程的run函数调用结束或者线程没有被启动,wait返回true。如果等待超时,此函数将返回false |
terminate()
|
终止线程的执行
|
finished()
|
当线程结束时会发出该信号
|
2、Qt线程安全
线程安全指的是多个线程修改共享资源时会发生意料之外的错误,而Qt中的共享资源毫无疑问是界面本身,而对界面的修改默认是在主线程,如果我们新建线程并对界面进行修改就会引发线程安全问题,因此Qt中严格规定新建的线程不能对界面进行修改!
若需要使用线程对界面进行修改,则采用信号与槽的方式,线程向主线程发送信号,主线程连接该信号与槽,在槽函数中进行对界面的修改,因为槽函数的执行是在主线程,因此不会出现线程安全问题。如下图:
3、使用线程QThread
用QLabel模拟一个时间表,首先在界面上创建一个按钮(pushbutton)和一个标签(label),按钮的作用是让线程开始执行任务,并将任务的结果打印到标签中,至此模拟出时间表。界面设计如下:
1、首先要实现上述功能,必须先创建一个继承自QThread的类,点击新建项目选择新建类:
2、然后自定义类名,并让该类继承QThread,如下:
3、创建完毕后会自动生成相关的文件和代码,我们需要做的是重写run函数,让该线程能够执行我们期望的任务,并且还要给该类自定义一个信号,目的是通过发送信号间接修改界面,mythread.h文件代码如下:
#ifndef MYTHREAD_H
#define MYTHREAD_H
#include <QWidget>
#include <QThread>
class mythread : public QThread
{
Q_OBJECT
public:
mythread();
void run();//重写run函数
signals:
void mythread_signal(QString time);//自定义的信号
};
#endif // MYTHREAD_H
mythread.cpp文件代码如下:
#include "mythread.h"
#include <QTime>
#include <QDebug>
mythread::mythread()
{
}
void mythread::run()
{
while(1)
{
QString time = QTime::currentTime().toString("hh:mm:ss");
qDebug()<<time;
emit mythread_signal(time);//将时间传过去
sleep(1);//每过一秒更新一次
}
}
4、上述线程的工作已经完成,现在需要在主线程中实例化线程对象,并在主线程中调用线程的start函数让线程跑起来,可以让按钮完成启动线程的工作,并且实现线程信号对应的槽函数,在该槽函数中进行对label的文本设置,widget.h文件代码如下:
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <mythread.h>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void on_pushButton_clicked();
void thread_slot(QString time);//线程信号的槽函数
private:
mythread thread;//要想启动线程,必须创建线程对象
Ui::Widget *ui;
};
#endif // WIDGET_H
widget.cpp代码如下:
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
connect(&thread,&mythread::mythread_signal,this,&Widget::thread_slot);
}
Widget::~Widget()
{
delete ui;
}
void Widget::on_pushButton_clicked()
{
thread.start();//启动线程
}
void Widget::thread_slot(QString time)
{
ui->label->setText(time);//设置文本
}
运行结果:
至此实现了使用线程将label标签设置为时间表的功能。整个程序的逻辑:点击按钮启动线程的start函数->线程开始执行run函数并不断的发送信号->主线程收到线程的信号后执行对应的槽函数实现label文本的更新。
4、connect函数的第五个参数
connect函数是用于连接信号与槽的,connect函数第五个参数为Qt::ConnectionType,用于指定信号与槽的连接类型,主要影响槽函数的执行逻辑,一般在多线程的情况下才会用到第五个参数。Qt::ConnectionType提供了以下五种方式:
Qt::AutoConnection
|
在Qt中,会根据信号和槽函数是否处于统一线程自动选择连接类型。如果信号和槽函数在同⼀线程中,就会Qt:DirectConnection类型;如果它们位于不同的线程中,则使用Qt::QueuedConnection类型。
|
Qt::DirectConnection
|
当信号发出时,槽函数会立即在同⼀线程中执行,可以理解为就是简单的一次函数调用。
|
Qt::QueuedConnection
|
当信号发出时,槽函数会被插⼊到接收对象所属的线程的事件队列中,等待下⼀次事件循环时执行。
|
Qt::BlockingQueuedConnection
|
与Qt:QueuedConnection类似,但是发送信号的线程会被阻塞,直到槽函数执行完毕。
|
Qt::UniqueConnection
|
这是⼀个标志,可以使用位运算和上述任何⼀种连接类型组合使用。
|
5、Qt互斥锁
讲到多线程就自然离不开互斥锁的使用,由于多线程很容易导致线程安全问题,因此使用互斥锁限制多个线程对共享资源的访问。在Qt中,互斥锁主要是通过QMutex类来实现。互斥锁的使用如下。
1、首先创建一个继承QThread的类thread,并在该类中创建一把锁和一个静态变量,thread.h文件代码如下:
#ifndef THREAD_H
#define THREAD_H
#include <QWidget>
#include <QThread>
#include <QMutex>
class mythread : public QThread
{
Q_OBJECT
public:
mythread();
void run();
static int num;//让多个线程对该值进行++
private:
static QMutex mutex;//让线程看到同一把锁
};
#endif // THREAD_H
2、定义run函数,thread.cpp代码如下:
#include "thread.h"
int mythread::num = 0;
QMutex mythread::mutex;
mythread::mythread()
{
}
void mythread::run()
{
for(int i = 0;i<100000;i++)
{
mutex.lock();
num++;
mutex.unlock();
}
}
3、在widget.cpp中创建两个线程,这两个线程同时对num进行++操作,最终观察num的值,代码如下:
#include "widget.h"
#include "ui_widget.h"
#include "thread.h"
#include <QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
mythread t1;
mythread t2;
t1.start();
t2.start();
t1.wait();
t2.wait();
qDebug()<<mythread::num;
}
Widget::~Widget()
{
delete ui;
}
运行结果:
此时结果是预期的,因为对访问共享资源进行加锁限制,如果上述代码没有加锁,结果如下:
从这里也可以看到加锁的必要性。
5.1 QMutexLocker
QMutexLocker是QMutex的辅助类,使用RAII(Resource Acquisition Is Initialization)方式 对互斥锁进行自动上锁和解锁的操作。具体来说,他会在被创建时自动上锁,在作用域结束后自动释放锁,避免开发者忘记解锁导致的死锁等问题。
将上述代码的上锁解锁操作用QMutexLocker代替:
#include "thread.h"
int mythread::num = 0;
QMutex mythread::mutex;
mythread::mythread()
{
}
void mythread::run()
{
for(int i = 0;i<100000;i++)
{
QMutexLocker locker(&mutex);//创建时自动加锁
//mutex.lock();
num++;
//mutex.unlock();
}//出了作用域后自动结束
}
运行结果:
6、条件变量
在Qt中,QWaitCondition类表示条件变量,条件变量是作用是让线程实现同步机制,线程同步是为了让所有线程申请到锁的能力是一样的,上述例子中,线程t1和线程t2虽然实现了互斥,但是不具备同步机制,可以通过观察执行线程的地址来判断申请锁的能力,如下图:
添加条件变量后,节选widget.cpp代码:
#include "widget.h"
#include "ui_widget.h"
#include "thread.h"
#include <QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
mythread t1;
mythread t2;
t1.start();
t2.start();
for(int i = 0;i<1000;i++)
{
_sleep(1);
mythread::cond.wakeAll();//唤醒条件变量
}
t1.wait();
t2.wait();
qDebug()<<mythread::num;
}
Widget::~Widget()
{
delete ui;
}
测试结果:
可以发现两个线程依次有序的对共享资源进行++操作。
7、信号量
信号量类似于加强版互斥锁,不仅能完成上锁和解锁操作,而且还可以限制访问共享资源的线程数量。Qt中用QSemaphore类来表示信号量,用于控制同时访问共享资源的线程数量,测试代码如下:
#include "thread.h"
#include <QDebug>
#include <QSemaphore>
int mythread::num = 0;
QMutex mythread::mutex;
QWaitCondition mythread::cond;//初始化条件变量
QSemaphore QS(1);//信号量为1,表示只能有一个线程申请到信号量
mythread::mythread()
{
}
void mythread::run()
{
for(int i = 0;i<1000;i++)
{
QS.acquire();//尝试获取信号量,若已满则阻塞
//QMutexLocker locker(&mutex);
//cond.wait(&mutex);//条件变量等待
//mutex.lock();
num++;
qDebug()<<this;
//mutex.unlock();
QS.release();//释放信号量
}
}
结语
以上就是关于Qt线程介绍与使用,在Qt中使用线程最为重要的就是不能直接在线程中对界面进行修改,并且Qt线程采用的是继承的思想,和C++中的线程库采用回调的方式有些不同。线程的其他相关概念都相差无几。
最后如果本文有遗漏或者有误的地方欢迎大家在评论区补充,谢谢大家!!