天天看点

Qt 多线程编程之入门前言正文

前言

多线程在实际项目编程中是非常常用的,下面按照以下几点进行介绍:

  1. 线程介绍
  2. QThread介绍
  3. 线程同步

正文

1. 线程介绍

因为进程的创建、切换需要大量的时间和空间的消耗,因此在上世纪80年代诞生了一种SMP(Symmetrical Multi-Processing)的对称多处理技术,这就是线程。

线程的创建、切换所需的消耗要比进程小很多,这是因为进程需要一块完整的地址空间,而线程只是进程中的最小执行单元,共享进程的地址空间,不需要创建新的资源。

1.1 线程和进程的区别

区别 线程 进程
根本区别 是处理器任务调度和执行的最小单位 是操作系统资源调度的最小单位
资源开销 同进程下的线程共享进程的资源 进程之间的资源相互独立
包含关系 线程是进程的最小执行单元 进程包含多个线程

1.2 线程调度

Windows操作系统在启动后就会开始检查可调度线程内核对象,选择一个线程的内核对象,将其上下文载入CPU寄存器,然后线程开始运行。

线程的调度机制是由调度算法决定的,通用调度算法有:

  1. 先进先出算法(FIFO)

    按照任务进入队列的顺序,依次调度。

  2. 最短耗时任务优先算法(SJF)

    按照任务的耗时长短进行调度,耗时短的任务优先调度。

  3. 时间片轮转算法(Round Robin)

    给队列中的每个任务一个时间片,第一个任务先执行,时间片结束后将任务放到队列末尾,然后切换到下个任务执行。

  4. 最大最小公平算法(Max-Min Fairness)

    平均分配资源;

    有剩余资源的则平均分配给其他任务;

    资源不够则等待;

  5. 多级反馈队列算法(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();
}
           

执行结果:

Qt 多线程编程之入门前言正文

从上面代码可以看出,QThread派生类中的槽函数是在创建该对象的线程中执行的。

注意:Qt有一个术语Thread Affinity(线程亲和力):QObject实例对象都和创建它的线程相关联,可以通过moveToThread来改变此对象以及其子对象的线程亲和力。

2.3 moveToThread()

Qt5开始官方推荐的线程使用方式就是继承QObject然后通过moveToThread()改变线程亲和力。

下面直接附上官方例子源码:

class Worker : public QObject
{
    Q_OBJECT

public slots:
    void doWork(const QString &parameter) {
        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);
		}
	}
};
           

结果:

Qt 多线程编程之入门前言正文

从上面图片可以看出,未使用互斥量时会导致线程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);
		}
	}
};
           

运行结果:

Qt 多线程编程之入门前言正文

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();
}
           

运行结果:

Qt 多线程编程之入门前言正文

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);
		}
	}
};
           

运行结果:

Qt 多线程编程之入门前言正文

这里可以看到和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();			// 有一个人必须进去,但是没有位置可用,他就一直在外面等待,直到有人出来