QThread
QThread类是Qt中用于线程管理的类,它提供了一些常用的API,用于创建、控制和管理线程。下面是QThread类的一些常用API的详细描述:
- 构造函数:QThread()、QThread(QObject *parent),用于创建一个新的QThread对象。如果传递了QObject类型的父对象,则QThread将成为该对象的子对象。
- start():用于启动线程。调用start()后,QThread会调用其中的run()函数。在run()函数中,可以编写线程的执行逻辑。
- run():该函数是QThread的入口函数,在start()被调用时自动运行。默认情况下,run()函数什么也不做,需要通过继承QThread并重写run()函数来实现自定义的线程执行逻辑。
- exit():用于退出线程。调用exit()后,QThread会停止运行,并向线程发送退出信号。可以通过重写线程的终止标志位(isInterruptionRequested())来实现线程结束的判定条件。
- quit():用于退出线程事件循环。在run()函数中执行quit()后,线程会停止事件循环并退出。通常在run()函数中会循环执行一段代码,对于一次性任务可以使用quit()退出线程。
- wait():阻塞当前线程,直到QThread线程终止。在某些情况下,你可能需要等待线程终止后再继续执行某些操作,使用wait()可以实现这一目的。
- currentThread():静态函数,返回当前线程的QThread对象指针。可以通过该函数获取当前线程对象,从而可以访问线程的属性和调用其成员函数。
- setPriority(QThread::Priority priority):设置线程的优先级。Qt提供了QThread::Priority枚举类,包含了不同的优先级选项。可以根据应用程序的需求设置线程的优先级。
- msleep(unsigned long msecs):静态函数,使当前线程暂停指定的毫秒数。可以通过调用msleep()函数在线程中加入延时操作。
- sleep(unsigned long secs):静态函数,使当前线程暂停指定的秒数。与msleep()类似,但单位为秒。
- quitOnLastWindowClosed(bool quit):静态函数,设置是否在最后一个窗口关闭后自动退出应用程序。默认情况下,该设置为true,即在最后一个窗口关闭后,应用程序会自动退出。可以通过设置为false来阻止这一退出行为。
- QThread::ThreadId threadId = QThread::currentThreadId(); 将线程ID打印出来: qDebug() << "Current Thread ID: " << threadId;
这些是QThread类的一些常用API,使用这些API可以方便地创建和管理线程,并实现多线程的应用程序。
QT多线程
在进行桌面应用程序开发的时候, 假设应用程序在某些情况下需要处理比较复杂的逻辑, 如果只有一个线程去处理,就会导致窗口卡顿,无法处理用户的相关操作。这种情况下就需要使用多线程,其中一个线程处理窗口事件,其他线程进行逻辑运算,多个线程各司其职,不仅可以提高用户体验还可以提升程序的执行效率。
在 qt 中使用了多线程,有些事项是需要额外注意的:
默认的线程在Qt中称之为窗口线程,也叫主线程,负责窗口事件处理或者窗口控件数据的更新 子线程负责后台的业务逻辑处理,子线程中不能对窗口对象做任何操作,这些事情需要交给窗口线程处理 主线程和子线程之间如果要进行数据的传递,需要使用Qt中的信号槽机制
- 线程类 QThread Qt 中提供了一个线程类,通过这个类就可以创建子线程了,Qt 中一共提供了两种创建子线程的方式,后边会依次介绍其使用方式。先来看一下这个类中提供的一些常用 API 函数:
1.1 常用共用成员函数
// QThread 类常用 API
// 构造函数
QThread::QThread(QObject *parent = Q_NULLPTR);
// 判断线程中的任务是不是处理完毕了
bool QThread::isFinished() const;
// 判断子线程是不是在执行任务
bool QThread::isRunning() const;
// Qt中的线程可以设置优先级
// 得到当前线程的优先级
Priority QThread::priority() const;
void QThread::setPriority(Priority priority);
优先级:
QThread::IdlePriority --> 最低的优先级
QThread::LowestPriority
QThread::LowPriority
QThread::NormalPriority
QThread::HighPriority
QThread::HighestPriority
QThread::TimeCriticalPriority --> 最高的优先级
QThread::InheritPriority --> 子线程和其父线程的优先级相同, 默认是这个
// 退出线程, 停止底层的事件循环
// 退出线程的工作函数
void QThread::exit(int returnCode = 0);
// 调用线程退出函数之后, 线程不会马上退出因为当前任务有可能还没有完成, 调回用这个函数是
// 等待任务完成, 然后退出线程, 一般情况下会在 exit() 后边调用这个函数
bool QThread::wait(unsigned long time = ULONG_MAX);1.2 信号槽
// 和调用 exit() 效果是一样的
// 代用这个函数之后, 再调用 wait() 函数
[slot] void QThread::quit();
// 启动子线程
[slot] void QThread::start(Priority priority = InheritPriority);
// 线程退出, 可能是会马上终止线程, 一般情况下不使用这个函数
[slot] void QThread::terminate();
// 线程中执行的任务完成了, 发出该信号
// 任务函数中的处理逻辑执行完毕了
[signal] void QThread::finished();
// 开始工作之前发出这个信号, 一般不使用
[signal] void QThread::started();1.3 静态函数
// 返回一个指向管理当前执行线程的QThread的指针
[static] QThread *QThread::currentThread();
// 返回可以在系统上运行的理想线程数 == 和当前电脑的 CPU 核心数相同
[static] int QThread::idealThreadCount();
// 线程休眠函数
[static] void QThread::msleep(unsigned long msecs); // 单位: 毫秒
[static] void QThread::sleep(unsigned long secs); // 单位: 秒
[static] void QThread::usleep(unsigned long usecs); // 单位: 微秒1.4 任务处理函数
// 子线程要处理什么任务, 需要写到 run() 中
[virtual protected] void QThread::run();
这个 run() 是一个虚函数,如果想让创建的子线程执行某个任务,需要写一个子类让其继承 QThread,并且在子类中重写父类的 run() 方法,函数体就是对应的任务处理流程。另外,这个函数是一个受保护的成员函数,不能够在类的外部调用,如果想要让线程执行这个函数中的业务流程,需要通过当前线程对象调用槽函数 start() 启动子线程,当子线程被启动,这个 run() 函数也就在线程内部被调用了。- 使用方式 1 2.1 操作步骤 Qt 中提供的多线程的第一种使用方式的特点是: 简单。操作步骤如下:
需要创建一个线程类的子类,让其继承 QT 中的线程类 QThread,比如:
class MyThread:public QThread
{
......
}重写父类的 run () 方法,在该函数内部编写子线程要处理的具体的业务流程
class MyThread:public QThread
{
......
protected:
void run()
{
........
}
}在主线程中创建子线程对象,new 一个就可以了
MyThread * subThread = new MyThread;启动子线程,调用 start () 方法
subThread->start();不能在类的外部调用 run () 方法启动子线程,在外部调用 start () 相当于让 run () 开始运行
当子线程别创建出来之后,父子线程之间的通信可以通过信号槽的方式,注意事项:
在 Qt 中在子线程中不要操作程序中的窗口类型对象,不允许,如果操作了程序就挂了 只有主线程才能操作程序中的窗口对象,默认的线程就是主线程,自己创建的就是子线程
2.2 示例代码 举一个简单的数数的例子,假如只有一个线程,让其一直数数,会发现数字并不会在窗口中时时更新,并且这时候如果用户使用鼠标拖动窗口,就会出现无响应的情况,使用多线程就不会出现这样的现象了。
在下面的窗口中,点击按钮开始在子线程中数数,让后通过信号槽机制将数据传递给 UI 线程,通过 UI 线程将数据更新到窗口中。
mythread.h
#ifndef MYTHREAD_H
#define MYTHREAD_H
#include <QThread>
class MyThread : public QThread
{
Q_OBJECT
public:
explicit MyThread(QObject *parent = nullptr);
protected:
void run();
signals:
// 自定义信号, 传递数据
void curNumber(int num);
public slots:
};
#endif // MYTHREAD_Hmythread.cpp
#include "mythread.h"
#include <QDebug>
MyThread::MyThread(QObject *parent) : QThread(parent)
{
}
void MyThread::run()
{
qDebug() << "当前线程对象的地址: " << QThread::currentThread();
int num = 0;
while(1)
{
emit curNumber(num++);
if(num == 10000000)
{
break;
}
QThread::usleep(1);
}
qDebug() << "run() 执行完毕, 子线程退出...";
}mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "mythread.h"
#include <QDebug>
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
qDebug() << "主线程对象地址: " << QThread::currentThread();
// 创建子线程
MyThread* subThread = new MyThread;
connect(subThread, &MyThread::curNumber, this, [=](int num)
{
ui->label->setNum(num);
});
connect(ui->startBtn, &QPushButton::clicked, this, [=]()
{
// 启动子线程
subThread->start();
});
}
MainWindow::~MainWindow()
{
delete ui;
}这种在程序中添加子线程的方式是非常简单的,但是也有弊端,假设要在一个子线程中处理多个任务,所有的处理逻辑都需要写到run()函数中,这样该函数中的处理逻辑就会变得非常混乱,不太容易维护。
- 使用方式 2 3.1 操作步骤 Qt 提供的第二种线程的创建方式弥补了第一种方式的缺点,用起来更加灵活,但是这种方式写起来会相对复杂一些,其具体操作步骤如下:
创建一个新的类,让这个类从 QObject 派生
class MyWork:public QObject
{
.......
}在这个类中添加一个公共的成员函数,函数体就是我们要子线程中执行的业务逻辑
class MyWork:public QObject
{
public:
.......
// 函数名自己指定, 叫什么都可以, 参数可以根据实际需求添加
void working();
}在主线程中创建一个 QThread 对象,这就是子线程的对象
QThread* sub = new QThread;在主线程中创建工作的类对象(千万不要指定给创建的对象指定父对象)
MyWork* work = new MyWork(this); // error
MyWork* work = new MyWork; // ok将 MyWork 对象移动到创建的子线程对象中,需要调用 QObject 类提供的 moveToThread() 方法
/ void QObject::moveToThread(QThread *targetThread);
// 如果给work指定了父对象, 这个函数调用就失败了
// 提示: QObject::moveToThread: Cannot move objects with a parent
work->moveToThread(sub); // 移动到子线程中工作启动子线程,调用 start(), 这时候线程启动了,但是移动到线程中的对象并没有工作
调用 MyWork 类对象的工作函数,让这个函数开始执行,这时候是在移动到的那个子线程中运行的
3.2 示例代码 假设函数处理上面在程序中数数的这个需求,具体的处理代码如下:
mywork.h
#ifndef MYWORK_H
#define MYWORK_H
#include <QObject>
class MyWork : public QObject
{
Q_OBJECT
public:
explicit MyWork(QObject *parent = nullptr);
// 工作函数
void working();
signals:
void curNumber(int num);
public slots:
};
#endif // MYWORK_H
https://subingwen.cn/qt/thread/?highlight=qtmywork.cpp
#include "mywork.h"
#include <QDebug>
#include <QThread>
MyWork::MyWork(QObject *parent) : QObject(parent)
{
}
void MyWork::working()
{
qDebug() << "当前线程对象的地址: " << QThread::currentThread();
int num = 0;
while(1)
{
emit curNumber(num++);
if(num == 10000000)
{
break;
}
QThread::usleep(1);
}
qDebug() << "run() 执行完毕, 子线程退出...";
}mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QThread>
#include "mywork.h"
#include <QDebug>
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
qDebug() << "主线程对象的地址: " << QThread::currentThread();
// 创建线程对象
QThread* sub = new QThread;
// 创建工作的类对象
// 千万不要指定给创建的对象指定父对象
// 如果指定了: QObject::moveToThread: Cannot move objects with a parent
MyWork* work = new MyWork;
// 将工作的类对象移动到创建的子线程对象中
work->moveToThread(sub);
// 启动线程
sub->start();
// 让工作的对象开始工作, 点击开始按钮, 开始工作
connect(ui->startBtn, &QPushButton::clicked, work, &MyWork::working);
// 显示数据
connect(work, &MyWork::curNumber, this, [=](int num)
{
ui->label->setNum(num);
});
}
MainWindow::~MainWindow()
{
delete ui;
}使用这种多线程方式,假设有多个不相关的业务流程需要被处理,那么就可以创建多个类似于 MyWork 的类,将业务流程放多类的公共成员函数中,然后将这个业务类的实例对象移动到对应的子线程中 moveToThread() 就可以了,这样可以让编写的程序更加灵活,可读性更强,更易于维护。
QT线程池
我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务呢?
线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件), 则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。
在各个编程语言的语种中都有线程池的概念,并且很多语言中直接提供了线程池,作为程序猿直接使用就可以了,下面给大家介绍一下线程池的实现原理:
线程池的组成主要分为 3 个部分,这三部分配合工作就可以得到一个完整的线程池:
任务队列,存储需要处理的任务,由工作的线程来处理这些任务 通过线程池提供的 API 函数,将一个待处理的任务添加到任务队列,或者从任务队列中删除 已处理的任务会被从任务队列中删除 线程池的使用者,也就是调用线程池函数往任务队列中添加任务的线程就是生产者线程 工作的线程(任务队列任务的消费者) ,N 个 线程池中维护了一定数量的工作线程,他们的作用是是不停的读任务队列,从里边取出任务并处理 工作的线程相当于是任务队列的消费者角色, 如果任务队列为空,工作的线程将会被阻塞 (使用条件变量 / 信号量阻塞) 如果阻塞之后有了新的任务,由生产者将阻塞解除,工作线程开始工作 管理者线程(不处理任务队列中的任务),1 个 它的任务是周期性的对任务队列中的任务数量以及处于忙状态的工作线程个数进行检测 当任务过多的时候,可以适当的创建一些新的工作线程 当任务过少的时候,可以适当的销毁一些工作的线程
- QRunnable 在 Qt 中使用线程池需要先创建任务,添加到线程池中的每一个任务都需要是一个 QRunnable 类型,因此在程序中需要创建子类继承 QRunnable 这个类,然后重写 run() 方法,在这个函数中编写要在线程池中执行的任务,并将这个子类对象传递给线程池,这样任务就可以被线程池中的某个工作的线程处理掉了。
QRunnable 类 常用函数不多,主要是设置任务对象传给线程池后,是否需要自动析构。
// 在子类中必须要重写的函数, 里边是任务的处理流程
[pure virtual] void QRunnable::run();
// 参数设置为 true: 这个任务对象在线程池中的线程中处理完毕, 这个任务对象就会自动销毁
// 参数设置为 false: 这个任务对象在线程池中的线程中处理完毕, 对象需要程序猿手动销毁
void QRunnable::setAutoDelete(bool autoDelete);
// 获取当前任务对象的析构方式,返回true->自动析构, 返回false->手动析构
bool QRunnable::autoDelete() const;创建一个要添加到线程池中的任务类,处理方式如下:
class MyWork : public QObject, public QRunnable
{
Q_OBJECT
public:
explicit MyWork(QObject *parent = nullptr)
{
// 任务执行完毕,该对象自动销毁
setAutoDelete(true);
}
~MyWork();
void run() override{}
}在上面的示例中 MyWork 类是一个多重继承,如果需要在这个任务中使用 Qt 的信号槽机制进行数据的传递就必须继承 QObject 这个类,如果不使用信号槽传递数据就可以不继承了,只继承 QRunnable 即可。
class MyWork :public QRunnable
{
Q_OBJECT
public:
explicit MyWork()
{
// 任务执行完毕,该对象自动销毁
setAutoDelete(true);
}
~MyWork();
void run() override{}
}- QThreadPool Qt 中的 QThreadPool 类管理了一组 QThreads, 里边还维护了一个任务队列。QThreadPool 管理和回收各个 QThread 对象,以帮助减少使用线程的程序中的线程创建成本。每个Qt应用程序都有一个全局 QThreadPool 对象,可以通过调用 globalInstance() 来访问它。也可以单独创建一个 QThreadPool 对象使用。
线程池常用的 API 函数如下:
// 获取和设置线程中的最大线程个数
int maxThreadCount() const;
void setMaxThreadCount(int maxThreadCount);
// 给线程池添加任务, 任务是一个 QRunnable 类型的对象
// 如果线程池中没有空闲的线程了, 任务会放到任务队列中, 等待线程处理
void QThreadPool::start(QRunnable * runnable, int priority = 0);
// 如果线程池中没有空闲的线程了, 直接返回值, 任务添加失败, 任务不会添加到任务队列中
bool QThreadPool::tryStart(QRunnable * runnable);
// 线程池中被激活的线程的个数(正在工作的线程个数)
int QThreadPool::activeThreadCount() const;
// 尝试性的将某一个任务从线程池的任务队列中删除, 如果任务已经开始执行就无法删除了
bool QThreadPool::tryTake(QRunnable *runnable);
// 将线程池中的任务队列里边没有开始处理的所有任务删除, 如果已经开始处理了就无法通过该函数删除了
void QThreadPool::clear();
// 在每个Qt应用程序中都有一个全局的线程池对象, 通过这个函数直接访问这个对象
static QThreadPool * QThreadPool::globalInstance();一般情况下,我们不需要在 Qt 程序中创建线程池对象,直接使用 Qt 为每个应用程序提供的线程池全局对象即可。得到线程池对象之后,调用 start() 方法就可以将一个任务添加到线程池中,这个任务就可以被线程池内部的线程池处理掉了,使用线程池比自己创建线程的这种多种多线程方式更加简单和易于维护。
具体的使用方式如下:
mywork.h
class MyWork :public QRunnable
{
Q_OBJECT
public:
explicit MyWork();
~MyWork();
void run() override;
}mywork.cpp
MyWork::MyWork() : QRunnable()
{
// 任务执行完毕,该对象自动销毁
setAutoDelete(true);
}
void MyWork::run()
{
// 业务处理代码
......
}mainwindow.cpp
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
// 线程池初始化,设置最大线程池数
QThreadPool::globalInstance()->setMaxThreadCount(4);
// 添加任务
MyWork* task = new MyWork;
QThreadPool::globalInstance()->start(task);
}QMutex
好的,我可以为您解释一下在C++的Qt框架中如何使用QMutex类。
在Qt中,QMutex(互斥锁)是一种用于线程同步的机制。它可以确保在多线程环境下,同一时刻只有一个线程可以访问共享数据。QMutex是Qt提供的一种线程安全的锁,它可以用来保护临界区,防止多个线程同时访问和修改同一个资源。
以下是使用QMutex类的一般步骤:
包含头文件:在使用
QMutex之前,您需要包含相应的头文件。在C++代码中,可以通过以下方式包含:cpp#include <QMutex>创建
QMutex对象:您需要创建一个QMutex对象来管理访问临界区。您可以在需要保护的类中或全局作用域中创建一个QMutex对象。例如:cpp// 在类中创建QMutex对象 class MyClass { private: QMutex mutex; // ... }; // 或者在全局作用域中创建QMutex对象 QMutex globalMutex;加锁和解锁:当您要访问或修改受
QMutex保护的资源时,您需要进行加锁和解锁操作。加锁时,如果其他线程已经获得了锁,当前线程将被阻塞,直到锁可用。一旦获取到锁,线程可以安全地访问临界区。解锁时,锁会释放,使其他线程可以获取锁并访问临界区。以下是在类中使用
QMutex进行加锁和解锁的示例代码:cppclass MyClass { private: QMutex mutex; int sharedData; public: void updateSharedData() { mutex.lock(); // 访问和修改受mutex保护的共享数据 // ... mutex.unlock(); } };如果是在全局作用域中使用
QMutex,可以使用lock()和unlock()直接调用全局QMutex对象的成员函数。使用
QMutexLocker类是更方便的一种方法,它可以自动在作用域结束时对QMutex进行解锁。以下是使用QMutexLocker的示例代码:cppclass MyClass { private: QMutex mutex; int sharedData; public: void updateSharedData() { QMutexLocker locker(&mutex); // 访问和修改受mutex保护的共享数据 // ... } };
使用QMutex类可以确保在多线程环境下对共享资源的安全访问。请注意,QMutex只能用于同一进程内的线程间同步,并且需要遵循正确的加锁和解锁操作,以避免死锁和其他同步问题。
希望上述解释对您有所帮助!如果有任何进一步的疑问,请随时提问。