目录
(一)多线程的概述
(二)Qt线程的使用条件
(三)创建线程的方法
3.1 继承QTread,重写run()函数
3.1.1 为什么要重写
3.2 继承QObject
3.3 核心API介绍
3.4 关闭线程的使用方法
(四)QThread常用函数
(五)线程同步工具
5.1 互斥锁
5.2 信号量
5.2.1 为什么需要信号量?
5.3 条件变量
5.3.1 为什么需要条件变量?
(六)总结
(一)多线程的概述
在Qt框架中,多线程编程是通过QThread类来实现的。QThread类提供了一种高级的、面向对象的方式来处理线程,它允许开发者将耗时的任务从主线程(GUI线程)中分离出来,以避免界面冻结和提高应用程序的响应性.
(二)Qt线程的使用条件
在Qt中,线程的使用通常是为了处理耗时的操作,以避免阻塞主界面(GUI)线程,从而保持应用程序的响应性。
- 1.耗时操作:当需要执行耗时的操作,如文件读写、网络请求、复杂计算等,这些操作可能会占用大量CPU时间或等待外部资源,这时应考虑使用线程。
- 2.保持界面响应:如果耗时操作在主线程中执行,可能会导致界面冻结或无响应.使用线程可以避免这种情况,让界面保持流畅。
- 3.并发任务:当需要同时执行多个任务时,可以使用线程来实现并发处理,提高程序的效率。
(三)创建线程的方法
在Qt中,常用的创建线程的方法如下:
- 方法⼀:继承QThread类,重写run()函数;
- 方法⼆:继承QObject类,通过moveToThread(thread),交给thread执行
3.1 继承QTread,重写run()函数
3.1.1 为什么要重写
在Qt中,创建线程通常通过继承QThread类并重写其run()方法来实现。这是因为QThread类本身并不执行任何任务,它的主要作用是提供线程管理的功能,如启动、停止、暂停和恢复线程。run()方法是线程的入口点,类似于主线程中的main()函数。run()方法是QThread的虚函数,重写run()方法允许你定义线程将要执行的具体任务.
使用方法⼀创建线程的步骤:
- 1. 自定义⼀个类,继承于QThread,并且只有⼀个线程处理函数(和主线程不是同⼀个线程),这个线 程处理函数主要就是重写⽗类中的run()函数。
- 2. 线程处理函数里面写⼊需要执行的复杂数据处理;
- 3. 启动线程不能直接调用run()函数,需要使用对象来调用start()函数实现线程启动;
- 4. 线程处理函数执行结束后可以定义⼀个信号来告诉主线程;
- 5. 最后关闭线程。
示例如下:
- 1、首先新建Qt项目,设计UI界面如下:
- 2、新建⼀个类,继承于QThread类;
程序如下:
/
//timethread.h
#ifndef TIMETHREAD_H
#define TIMETHREAD_H
#include <QWidget>
#include <QThread>
class TimeThread : public QThread
{
Q_OBJECT
public:
TimeThread();
void run(); //线程任务函数
signals:
void sendTime(QString Time); //声明信号函数
};
#endif // TIMETHREAD_H
//
// widget.h
/
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include "timethread.h"
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
void handle();
private:
Ui::Widget *ui;
TimeThread t; //定义线程对象
};
#endif // WIDGET_H
///
// timethread.cpp
//
#include "timethread.h"
#include <QTime>
#include <QDebug>
TimeThread::TimeThread()
{
}
void TimeThread::run()
{
while(1)
{
QString time = QTime::currentTime().toString("hh:mm:ss");
qDebug() << time;
emit sendTime(time); //发送信号
sleep(1);
}
}
/
// widget.cpp
#include "widget.h"
#include "ui_widget.h"
#include "timethread.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
connect(&t,&TimeThread::sendTime,this,&Widget::handle);
t.start();
}
Widget::~Widget()
{
delete ui;
}
void Widget::handle()
{
int value = ui->lcdNumber->intValue();
value--;
ui->lcdNumber->display(value);
}
执行效果:
3.2 继承QObject
使用方法二创建线程的步骤:
- 1. 自定义⼀个类,继承于QObject类;
- 2. 创建⼀个自定义线程类的对象,不能指定父对象;
- 3. 创建⼀个QThread类的对象,可以指定其父对象;
- 4. 将自定义线程对象加入到QThread类的对象中使用;
- 5. 使用start()函数启动线程。
- 6.关闭线程。
说明: 调用start() 函数只是启动了线程,但是并没有开启线程处理函数,线程处理函数的开启需要用到信号槽机制。
3.3 核心API介绍
moveToThread(QThread* targetThread) | 将⼀个对象移动到指定的线程中运行 |
isRunning() | 如果线程正在运行则返回true;否则返回false。 |
quit() | 告诉线程的事件循环以返回码0(success)退出。相当于调用 QThread::exit(0)。 如果线程没有事件循环,这个函数什么也不做。 |
wait(unsigned long time = ULONG_MAX | 阻塞线程,直到满足以下任何⼀个条件: 与此QThread对象关联的线程已经完成执行(即当它从run()返回时)。如 果线程已经完成,这个函数将返回true。如果线程尚未启动,它还返回 true。 已经过了几毫秒。如果时间是ULONG_MAX(默认值),那么等待永远不 会超时(线程必须从run()返回)。如果等待超时,此函数将返回false。 这提供了与POSIXpthread_join()函数类似的功能。 |
示例如下:
- 1、首先新建Qt项目,设计UI界面如下:
- 2、新建⼀个类,继承于Qobject类;
程序如下:
// mythread.h
///
#ifndef MYTHREAD_H
#define MYTHREAD_H
#include <QObject>
#include <QThread>
#include <QDebug>
class mythread : public QObject
{
Q_OBJECT
public:
explicit mythread(QObject *parent = nullptr);
void thread();
void setflag(bool flag = true);
signals:
void mysignal();
private:
bool isStop;
};
#endif // MYTHREAD_H
// mythread.cpp
///
mythread::mythread(QObject *parent) : QObject(parent)
{
isStop = false;
}
void mythread::thread()
{
while (!isStop)
{
QThread::sleep(1);
emit mysignal();
qDebug() << "⼦线程号:" << QThread::currentThread();
if(isStop)
{
break;
}
}
}
void mythread::setflag(bool flag)
{
isStop = flag;
}
///
// mywidgt.h
//
#ifndef MYWIDGET_H
#define MYWIDGET_H
#include <QWidget>
#include <mythread.h>
#include <QDebug>
QT_BEGIN_NAMESPACE
namespace Ui { class MyWidget; }
QT_END_NAMESPACE
class MyWidget : public QWidget
{
Q_OBJECT
public:
MyWidget(QWidget *parent = nullptr);
~MyWidget();
mythread *testthread;
QThread *thread;
signals:
void startsignal(); //启动⼦线程的信号
private slots:
void on_startPushbutton_clicked();
void delsignals();
void on_closePushbutton_clicked();
void dealclose();
private:
Ui::MyWidget *ui;
};#endif // MYWIDGET_
//mywidgt.cpp
#include "mywidget.h"
#include "ui_mywidget.h"
#include <QThread>
MyWidget::MyWidget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::MyWidget)
{
ui->setupUi(this);
//动态分配空间,不能指定⽗对象
testthread = new mythread();
//创建⼦线程
thread = new QThread(this);
//将⾃定义的线程加⼊到⼦线程中
testthread->moveToThread(thread);
connect(testthread, &mythread::mysignal, this, &MyWidget::delsignals);
qDebug() << "主线程号:" << QThread::currentThread();
connect(this, &MyWidget::startsignal, testthread, &mythread::thread);
connect(this, &MyWidget::destroyed, this, &MyWidget::dealclose);
}
MyWidget::~MyWidget()
{
delete ui;
}
void MyWidget::on_startPushbutton_clicked()
{
if(thread->isRunning() == true)
{
return;
}
//启动线程但是没有启动线程处理函数
thread->start();
//不能直接调⽤线程处理函数直接调⽤会导致线程处理函数和主线程处于同⼀线程
emit startsignal();
}
void MyWidget::delsignals()
{
static int i = 0;
i ++;
ui->lcdNumber->display(i);
}
void MyWidget::dealclose()
{
/* 释放对象*/
delete testthread;
on_closePushbutton_clicked();
}
执行效果如下:
说明:
- 1、线程函数内部不允许操作UI图形界⾯,⼀般用数据处理;
- 2、connect() 函数第五个参数表示的为连接的方式,且只有在多线程的时候才意义。
connect()函数第五个参数为Qt::ConnectionType,用于指定信号和槽的连接类型。同时影响信号的传递方式和槽函数的执行顺序。Qt::ConnectionType提供了以下五种方式:
3.4 关闭线程的使用方法
//此函数直接关闭线程不等待线程任务结束
void terminate();
//此函数等待线程任务结束之后关闭线程
void quit()
(四)QThread常用函数
QThread是Qt框架中用于处理多线程的核心类。它提供了一系列方法和信号,用于创建和管理线程。以下是一些QThread中常用的方法和信号的小结:
构造函数
- QThread::QThread(QObject *parent = nullptr): 构造函数用于创建一个线程对象。可以指定父象。
常用方法
- void QThread::start(QThread::Priority priority = InheritPriority): 启动线程。这个方法会创建一个新的线程,并在新线程中调用run()方法。
priority参数用于设置线程的优先级。
- void QThread::quit(): 请求线程退出。这个方法会设置线程的退出标志,当线程的run()方法返回时,线程会自动退出。
- void QThread::terminate(): 强制终止线程。这个方法会立即停止线程,不保证线程资源的正确释放,因此不推荐使用,除非在紧急情况下。
- void QThread::wait(unsigned long time = ULONG_MAX): 等待线程结束。这个方法会阻塞调用它的线程,直到目标线程结束或超时。
线程控制信号
- void QThread::started(): 当线程开始执行时发出此信号。
- void QThread::finished(): 当线程执行完毕时发出此信号。
- void QThread::terminated(): 当线程被强制终止时发出此信号。
线程状态
- bool QThread::isRunning() const: 检查线程是否正在运行。
- bool QThread::isFinished() const: 检查线程是否已经结束。
- bool QThread::isInterruptionRequested() const: 检查是否请求了线程中断。
线程优先级
- void QThread::setPriority(QThread::Priority priority) :设置线程的优先级。
- QThread::Priority QThread::priority() const: 获取线程的当前优先级。
(五)线程同步工具
线程并行会导致资源竞争,线程同步工具会解决这些冲突。
实现线程互斥和同步常用的类有:
- 互斥锁:QMutex、QMutexLocker
- 条件变量:QWaitCondition • 信号量:QSemaphore
- 读写锁:QReadLocker、QWriteLocker、QReadWriteLock
5.1 互斥锁
当多个线程访问和修改共享资源时,如果不加以适当的控制,就可能产生竞态条件、数据不一致甚至程序崩溃等问题。为了防止这些问题,Qt提供了多种同步机制,其中互斥锁(Mutex)是最基本也是最常用的同步工具之一。
互斥锁是⼀种保护和防止多个线程同时访问同⼀对象实例的方法,在Qt中,互斥锁主要是通过 QMutex类来处理。
QMutex:
- 特点:QMutex是Qt框架提供的互斥锁类,用于保护共享资源的访问,实现线程间的互斥操作。
- 用途:在多线程环境下,通过互斥锁来控制对共享数据的访问,确保线程安全。
QMutex mutex;
mutex.lock(); //上锁
//
访问共享资源
//...
mutex.unlock(); //解锁
QMutexLocker
- 特点:QMutexLocker是QMutex的辅助类,使⽤RAII(ResourceAcquisitionIsInitialization)方式 对互斥锁进行上锁和解锁操作。
- 用途:简化对互斥锁的上锁和解锁操作,避免忘记解锁导致的死锁等问题。
QMutex mutex;
{
QMutexLocker locker(&mutex); //在作⽤域内⾃动上锁
//访问共享资源
//...
} //在作⽤域结束时⾃动解锁
QReadWriteLocker、QReadLocker、QWriteLocker
-
特点: QReadWriteLock 是读写锁类,用于控制读和写的并发访问。 QReadLocker用于读操作上锁,允许多个线程同时读取共享资源。 QWriteLocker 用于写操作上锁,只允许⼀个线程写入共享资源。
-
用途:在某些情况下,多个线程可以同时读取共享数据,但只有⼀个线程能够进行写操作。读写锁提供了更高效的并发访问方式。
QReadWriteLock rwLock;
//在读操作中使⽤读锁
{
QReadLocker locker(&rwLock); //在作⽤域内⾃动上读锁
//读取共享资源
//...
} //在作⽤域结束时⾃动解读锁
//在写操作中使⽤写锁
{
//修改共享资源
//...
QWriteLocker locker(&rwLock); //在作⽤域内⾃动上写锁
} //在作⽤域结束时⾃动解写锁
示例如下:
/
// myThread.h
#ifndef MYTHREAD_H
#define MYTHREAD_H
#include <QThread>
#include <QMutex>
class myThread : public QThread
{
Q_OBJECT
public:
explicit myThread(QObject *parent = nullptr);
void run();
private:
static QMutex mutex; //多个线程使⽤⼀把锁
static int num; //多个线程访问⼀个数据
};
#endif // MYTHREAD_H
/
// myThread.cpp
#include "mythread.h"
#include <QDebug>
QMutex myThread::mutex;
int myThread::num = 0;
myThread::myThread(QObject *parent) : QThread(parent)
{
}
void myThread::run()
{
while(1)
{
this->mutex.lock(); //加锁
qDebug() << "Current Thread: " << this << ", Value: " << this->num++;
this->mutex.unlock(); //解锁
QThread::sleep(1); //线程睡眠两秒
}
}
// mainwindow.h
///
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H
//
// mainwindow.cpp
/
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "mythread.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
myThread *t1 = new myThread(this);
myThread *t2 = new myThread(this);
t1->start();
t2->start();
}
MainWindow::~MainWindow()
{
delete ui;
}
执行效果:
- 两个线程使用⼀把锁,操作⼀个数据,数据会被两个线程依次打印:0、1、2、3、4...
示例:在上述示例的基础上使⽤QMutexLocker锁。
//
// myThread.cpp
/
#include "mythread.h"
#include <QDebug>
QMutex myThread::mutex;
int myThread::num = 0;
myThread::myThread(QObject *parent) : QThread(parent)
{
}
void myThread::run()
{
while (1)
{
//QMutexLocker:创建的时候加锁,当QMutexLocker局部销毁的时候解锁
{
QMutexLocker lock(&this->mutex);
qDebug() << "Current Thread: " << this << ", Value: " << this->num++;
QThread::sleep(1);// 线程睡眠两秒
}
}
}
5.2 信号量
5.2.1 为什么需要信号量?
在多线程环境中,多个线程可能需要访问有限的共享资源。如果没有适当的同步机制,就可能出现多个线程同时访问同一资源的情况,导致数据损坏或不一致。
Qt提供了QSemaphore类来实现信号量。QSemaphore类提供了以下基本操作:
- acquire(int n):尝试获取n个信号量。如果信号量的值小于n,则调用线程将被阻塞,直到信号量的值足够大。
- release(int n):释放n个信号量,增加信号量的计数器。
- available():返回当前可用的信号量数量。
QSemaphore
- 特点:QSemaphore是Qt框架提供的计数信号量类,用于控制同时访问共享资源的线程数量。
- 用途:限制并发线程数量,用于解决⼀些资源有限的问题
QSemaphore semaphore(2); //同时允许两个线程访问共享资源
//在需要访问共享资源的线程中
semaphore.acquire(); //尝试获取信号量,若已满则阻塞
//访问共享资源
//...
semaphore.release(); //释放信号量
//在另⼀个线程中进⾏类似操作
5.3 条件变量
5.3.1 为什么需要条件变量?
在多线程环境中,线程可能需要等待某个条件成立才能继续执行。例如,一个线程可能需要等待另一个线程完成特定的任务或更新共享数据。没有条件变量,线程可能需要不断轮询检查条件是否满足,这会导致资源浪费和效率低下。条件变量提供了一种机制,允许线程在条件不满足时挂起,直到其他线程通知条件已满足。
Qt提供了QWaitCondition类来实现条件变量。QWaitCondition类提供了以下基本操作:
- wait(QMutex *mutex):使当前线程等待条件变量。线程在调用此方法时必须持有与条件变量关联的互斥锁。线程将释放互斥锁并进入等待状态,直到其他线程调用wakeOne()或wakeAll()方法唤醒它。
- wakeOne():唤醒一个等待条件变量的线程。被唤醒的线程将尝试重新获取互斥锁,并继续执行。
- wakeAll():唤醒所有等待条件变量的线程。所有被唤醒的线程将尝试重新获取互斥锁,并继续执行。
QWaitCondition
- 特点:QWaitCondition是Qt框架提供的条件变量类,用于线程之间的消息通信和同步。
- 用途:在某个条件满足时等待或唤醒线程,用于线程的同步和协调。
QMutex mutex;
QWaitCondition condition;
//在等待线程中
mutex.lock();
//检查条件是否满⾜,若不满⾜则等待
while (!conditionFullfilled())
{
condition.wait(&mutex); //等待条件满⾜并释放锁
}
//条件满⾜后继续执⾏
//...
mutex.unlock();
//在改变条件的线程中
mutex.lock();
//改变条件
changeCondition();
condition.wakeAll(); //唤醒等待的线程
mutex.unlock();
(六)总结
多线程的概述
- 多线程是现代操作系统和编程语言提供的一个核心特性,它允许程序同时执行多个任务,从而提高应用程序的效率和响应性。在多线程环境中,每个线程可以看作是程序中的一个独立执行路径,它们可以并发执行,共享进程资源,但同时拥有自己的调用栈和程序计数器。多线程编程的挑战在于确保线程安全,避免竞态条件、死锁和资源冲突等问题。
Qt线程的使用条件
Qt框架支持多线程编程,但使用线程时需要考虑以下条件:
- 1. **耗时操作**:当需要执行耗时的操作,如文件读写、网络请求、复杂计算等,这些操作可能会占用大量CPU时间或等待外部资源,这时应考虑使用线程。
- 2. **保持界面响应**:如果耗时操作在主线程中执行,可能会导致界面冻结或无响应。使用线程可以避免这种情况,让界面保持流畅。
创建线程的方法
在Qt中,创建线程通常涉及以下步骤:
- 1. **继承QThread**重写run()方法
- 2. “继承QObject”
QThread常用函数
`QThread`类提供了一系列方法来管理线程的生命周期:
- `start()`:启动线程,调用`run()`方法。
- `quit()`:请求线程退出。
- `terminate()`:强制终止线程。
- `wait(unsigned long time = ULONG_MAX)`:等待线程结束。
- `isRunning()`:检查线程是否正在运行。
- `isFinished()`:检查线程是否已经结束。
线程同步工具
为了确保多线程程序的线程安全,Qt提供了多种同步工具:
- **QMutex**:互斥锁,用于保护共享资源,确保在任何时刻只有一个线程可以访问。
- **QMutexLocker**:辅助类,用于自动管理互斥锁的锁定和解锁。
- **QSemaphore**:信号量,用于控制对一组资源的访问。
- **QWaitCondition**:等待条件,允许线程在某些条件满足时被唤醒。
在设计多线程程序时,始终要考虑到线程安全和资源同步,以确保应用程序的正确性和效率。