前言
多线程在实际项目编程中是非常常用的,下面按照以下几点进行介绍:
- 线程介绍
- QThread介绍
- 线程同步
正文
1. 线程介绍
因为进程的创建、切换需要大量的时间和空间的消耗,因此在上世纪80年代诞生了一种SMP(Symmetrical Multi-Processing)的对称多处理技术,这就是线程。
线程的创建、切换所需的消耗要比进程小很多,这是因为进程需要一块完整的地址空间,而线程只是进程中的最小执行单元,共享进程的地址空间,不需要创建新的资源。
1.1 线程和进程的区别
区别 | 线程 | 进程 |
---|---|---|
根本区别 | 是处理器任务调度和执行的最小单位 | 是操作系统资源调度的最小单位 |
资源开销 | 同进程下的线程共享进程的资源 | 进程之间的资源相互独立 |
包含关系 | 线程是进程的最小执行单元 | 进程包含多个线程 |
1.2 线程调度
Windows操作系统在启动后就会开始检查可调度线程内核对象,选择一个线程的内核对象,将其上下文载入CPU寄存器,然后线程开始运行。
线程的调度机制是由调度算法决定的,通用调度算法有:
-
先进先出算法(FIFO)
按照任务进入队列的顺序,依次调度。
-
最短耗时任务优先算法(SJF)
按照任务的耗时长短进行调度,耗时短的任务优先调度。
-
时间片轮转算法(Round Robin)
给队列中的每个任务一个时间片,第一个任务先执行,时间片结束后将任务放到队列末尾,然后切换到下个任务执行。
-
最大最小公平算法(Max-Min Fairness)
平均分配资源;
有剩余资源的则平均分配给其他任务;
资源不够则等待;
-
多级反馈队列算法(Multi-level Feedback Queue)
算法按照优先级从高到低排序,优先级越低分片时长越大;
高优先级任务可以抢占低优先级任务;
任务在一个时间片结束时,若任务结束,则正常退出系统,若任务还没结束,则降低一个等级;
任务如果因为等待I/O响应而主动让出CPU,则保留当前等级(或者是提高一个等级);
同一等级的任务采用时间片轮转算法(Round Robin);
2. QThread介绍
QThread类提供不依赖平台的管理线程的方法。一个QThread类的对象管理一个线程,一般实现方式有两种:
- 继承QThread类,并重定义虚函数run(),在run()函数实现耗时任务
- 继承QObject类,自定义槽函数实现耗时任务,通过moveToThread来改变对象的线程亲和力
2.1 QThread基础
QThread是Qt线程中的抽象类,所有的线程类都是从QThread抽象类中派生出来的。
下面列举主要函数:
类型 | 函数 | 功能 |
---|---|---|
public | void setPriority(Priority priority) | 设置线程的优先级 |
public | void exit(int returenCode = 0) | 退出线程的事件循环 |
public | bool wait(unsigned long time) | 阻塞等待线程结束,直到time毫秒后 |
public slot | void quit() | 退出线程的事件循环 |
public slot | void start(Priority priority) | 内部调用run()开始执行线程,操作系统根据priority参数进行调度 |
public slot | void terminate() | 终止线程的运行,不是立即结束线程,而是等待操作系统结束线程。使用terminate()之后应使用wait() |
signal | void finished() | 在线程结束时发送该信号 |
signal | void started() | 在线程开始执行时,run()被调用之前发送该信号 |
protected | virtual void run() | start()调用run()开始线程任务的执行,在run()中实现线程的处理任务 |
protected | int exec() | 由run()调用,进入线程的事件循环,等待exit()退出 |
QThread线程有8个优先级:
优先级 | 注释 |
---|---|
QThread::IdlePriority | 仅在没有其他线程运行的情况下执行 |
QThread::LowestPriority | 优先级低于LowPriority |
QThread::LowPriority | 优先级低于NormalPriority |
QThread::NormalPriority | 操作系统的默认优先级 |
QThread::HighPriority | 优先级高于NormalPriority |
QThread::HighestPriority | 优先级高于HighPriority |
QThread::TimeCriticalPriority | 尽可能频繁运行 |
QThread::InheritPriority | 使用与创建线程相同的优先级(默认) |
2.2 继承QThread类
按照官网的介绍,主要是以下两点:
- 不在run()中调用exec(),就不会运行事件循环
- QThread派生的实例对象除了run()在新线程中执行外,其他(构造函数、槽函数等)都在创建该对象的线程中执行
下面测试一下:
class WorkerThd : public QThread
{
Q_OBJECT
public:
WorkerThd(QObject* parent = nullptr) : QThread(parent) {}
void run() override {
qDebug() << "ThreadID of the run(): " << QThread::currentThreadId();
}
Q_SLOT void doOtherWork() {
qDebug() << "ThreadID of the doOtherWork(): " << QThread::currentThreadId();
}
};
class Controller : public QObject
{
Q_OBJECT
public:
Controller(QObject* parent = nullptr) : QObject(parent) {
workThd = new WorkerThd(this);
connect(this, &Controller::sigDoOtherWork, workThd, &WorkerThd::doOtherWork);
connect(workThd, &QThread::finished, workThd, &QObject::deleteLater);
workThd->start();
}
~Controller() {
workThd->quit();
workThd->wait();
}
WorkerThd* workThd;
signals:
void sigDoOtherWork();
};
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
qDebug() << "ThreadID of the main(): " << QThread::currentThreadId();
Controller control;
emit control.sigDoOtherWork();
return a.exec();
}
执行结果:
从上面代码可以看出,QThread派生类中的槽函数是在创建该对象的线程中执行的。
注意:Qt有一个术语Thread Affinity(线程亲和力):QObject实例对象都和创建它的线程相关联,可以通过moveToThread来改变此对象以及其子对象的线程亲和力。
2.3 moveToThread()
Qt5开始官方推荐的线程使用方式就是继承QObject然后通过moveToThread()改变线程亲和力。
下面直接附上官方例子源码:
class Worker : public QObject
{
Q_OBJECT
public slots:
void doWork(const QString ¶meter) {
QString result;
/* ... here is the expensive or blocking operation ... */
emit resultReady(result);
}
signals:
void resultReady(const QString &result);
};
class Controller : public QObject
{
Q_OBJECT
QThread workerThread;
public:
Controller() {
Worker *worker = new Worker;
worker->moveToThread(&workerThread);
connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater);
connect(this, &Controller::operate, worker, &Worker::doWork);
connect(worker, &Worker::resultReady, this, &Controller::handleResults);
workerThread.start();
}
~Controller() {
workerThread.quit();
workerThread.wait();
}
public slots:
void handleResults(const QString &);
signals:
void operate(const QString &);
};
注意:Worker类的实例对象不能指定父对象,指定父对象后,moveToThread将无法改变其线程亲和力。
3. 线程同步
在多线程编程中频繁遇到的一个问题就是线程同步问题,什么是线程同步呢?
线程同步是因为在多个线程都要同时访问(读和写)一个全局变量,会导致变量的状态出现混乱,且不可控,从而导致程序异常。
Qt提供了以下几种方法来解决线程同步问题:
- 3.1 基于互斥量的线程同步
- 3.2 基于QReadWriteLock的线程同步
- 3.3 基于QWaitCondition的线程同步
- 3.4 基于信号量的线程同步
3.1 基于互斥量的线程同步
QMutex和QMutexLocker是基于互斥量的线程同步类。
QMutex定义的实例是一个互斥量,QMutex主要提供3个函数:
- lock():锁定互斥量,如果另外一个线程锁定了这个互斥量,它将阻塞执行直到其他线程解锁这个互斥量
- unlock():解锁一个互斥量,需要与lock()配对使用
-
tryLock():试图锁定一个互斥量,返回true表示成功锁定互斥量,返回false表示其他线程已经锁定互斥量,不管返回结果是什么都不会阻塞程序执行
假设两个线程需要一直打印信息,且用的是同一个打印接口,下面列举了使用互斥量和未使用互斥量的结果:
打印接口代码:
extern void debugMsg(int num)
{
qDebug() << "abc" << num;
qDebug() << "xyz" << num;
}
线程代码:
extern QMutex mutex_Num;
class MutexThd1 : public QThread
{
Q_OBJECT
public:
void run() override {
while (true)
{
debugMsg(1);
msleep(1000);
}
}
};
class MutexThd2 : public QThread
{
Q_OBJECT
public:
void run() override {
while (true)
{
debugMsg(2);
msleep(1000);
}
}
};
结果:
从上面图片可以看出,未使用互斥量时会导致线程1或者线程2在运行过程中被抢占,导致打印数据异常。
当使用互斥量时:
extern QMutex mutex_Num;
extern void debugMsg(int num);
class MutexThd1 : public QThread
{
Q_OBJECT
public:
void run() override {
while (true)
{
if (mutex_Num.tryLock())
{
debugMsg(1);
mutex_Num.unlock();
}
msleep(1000);
}
}
};
class MutexThd2 : public QThread
{
Q_OBJECT
public:
void run() override {
while (true)
{
if (mutex_Num.tryLock())
{
debugMsg(2);
mutex_Num.unlock();
}
msleep(1000);
}
}
};
运行结果:
QMutexLocker类是用来简化QMutex的lock()和unlock()操作的。
设想一种情况,当lock()之后的代码包含了异常处理代码,遇到异常则结束。那么unlock()操作需要在所有异常的地方添加相应代码,这样导致代码的复杂度,也更容易出错(在异常判断后忘记unlock()操作)。
示例代码:
extern QMutex mutex_Num;
class MutexThd1 : public QThread
{
Q_OBJECT
public:
void run() override {
while (true)
{
QMutexLocker locker(&mutex_Num);
debugMsg(1);
msleep(1000);
}
}
};
原理:
在QMutexLocker局部变量被创建的时候上锁,当该变量被销毁时自动解锁。
所以QMutexLocker类只能定义为局部变量。
3.2 基于QReadWriteLock的线程同步
使用互斥量的时候存在一个问题:每次只能有一个线程获得互斥量的控制权限。如果在一个程序中有多个线程读取变量,使用互斥量时必须排队获取。而实际上只是读取一个变量是可以让多个线程同时访问的,这样互斥量就会降低程序的性能。
QReadWriteLock类提供读写锁。
读写锁主要用于允许多个线程同时以只读的形式访问。
下面列举相关代码:
extern QString globalMsg;
extern void writeMsg(const QString& msg)
{
globalMsg = msg;
}
extern const QString& readMsg()
{
return globalMsg;
}
extern QReadWriteLock lock;
class WriteThd : public QThread
{
Q_OBJECT
public:
void run() override {
int index = 0;
while (true)
{
lock.lockForWrite();
writeMsg(QString("Message No.%1").arg(index++));
lock.unlock();
msleep(600);
}
}
};
class ReadThd : public QThread
{
Q_OBJECT
public:
void run() override {
int index = 0;
while (true)
{
lock.lockForRead();
qDebug() << readMsg();
lock.unlock();
msleep(300);
}
}
};
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
/*测试读写锁*/
WriteThd wThd;
ReadThd rThd1, rThd2;
wThd.start();
rThd1.start();
rThd2.start();
return a.exec();
}
运行结果:
3.3 基于QWaitCondition的线程同步
在上面例子中,假设WriteThd线程写完之后才能让ReadThd线程去读。这里就需要涉及到线程之间的通知了,当WriteThd线程写完后通知ReadThd线程读取。只通过互斥量和QReadWriteLock锁的方法可以对资源进行锁定和解锁,避免冲突,但是无法做到通知其他线程。
QWaitCondition提供了一种改进线程同步的方法,QWaitCondition和QMutex结合,可以使一个线程在满足一定条件时,通知其他多个线程,使它们及时做出响应。
相关函数介绍:
函数 | 功能 |
---|---|
bool wait(QMutex *lockedMutex) | 解锁互斥量lockedMutex,并阻塞等待唤醒条件,被唤醒后锁定lockedMutex并退出 |
void wakeAll() | 唤醒所有处于等待状态的线程,线程唤醒的顺序不确定,由操作系统的调度策略决定 |
void wakeOne() | 唤醒一个处于等待状态的线程,唤醒哪个线程不确定,由操作系统的调度策略决定 |
测试代码:
extern QMutex lock;
extern QWaitCondition dataCondition;
class WriteThd : public QThread
{
Q_OBJECT
public:
void run() override {
int index = 0;
while (true)
{
lock.lock();
writeMsg(QString("Message No.%1").arg(index++));
lock.unlock();
dataCondition.wakeAll();
msleep(600);
}
}
};
class ReadThd : public QThread
{
Q_OBJECT
public:
void run() override {
int index = 0;
while (true)
{
lock.lock();
dataCondition.wait(&lock);
qDebug() << readMsg();
lock.unlock();
msleep(300);
}
}
};
运行结果:
这里可以看到和QReadWriteLock使用的效果不同了,读线程只有在写线程完成之后才会被触发读操作。
3.4 基于信号量的线程同步
信号量(Semaphore)是另一种限制对共享资源进行访问的线程同步机制,它与互斥量(Mutex)相似,但是有区别。一个互斥量只能被锁定依次,而信号量可以多次使用。信号量通常用来保护一定数量的相同资源。
函数介绍:
函数 | 功能 |
---|---|
acquire(int n) | 尝试获得n个资源,如果资源不足n个,则线程将阻塞等待直到有n个资源可用 |
release(int n) | 释放n个资源,如果信号量的资源已全部可用之后再release,就可用创建更多的资源,增加可用资源的个数 |
int available() | 返回当前信号量可用的资源个数,如果为0则表示当前没有可用的资源 |
bool tryAcquire(int n = 1) | 尝试获取n个资源,不成功时不会阻塞线程 |
下面通过代码来举个例子,公共厕所作为共享资源:
QSemaphore wc(5); // 初始化厕所有5个位置
wc.acquire(4); // 有4个人进去了,占用了4个位置,还剩余1个位置可用
wc.release(2); // 有2个人出来了,还剩余3个位置可用
wc.acquire(3); // 有3个人进去了,还剩余0个位置可用
wc.tryAcquire(1); // 有一个人尝试进去,但是没有位置可用,他没有等待直接走了
wc.acquire(); // 有一个人必须进去,但是没有位置可用,他就一直在外面等待,直到有人出来