目录
[1. Qt 多线程概述](#1. Qt 多线程概述)
[2. QThread 常用 API](#2. QThread 常用 API)
[3. 使用线程](#3. 使用线程)
[4. 线程安全](#4. 线程安全)
[4.1 互斥锁](#4.1 互斥锁)
[4.2 条件变量](#4.2 条件变量)
[4.3 信号量](#4.3 信号量)
1. Qt 多线程概述
Qt 多线程 和 Linux 中的线程 本质是一个东西。Linux 谈到的各种和线程相关的 原理 和 注意事项 都是在 Qt 中使用的!!!
2. QThread 常用 API
|-------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| run() | 线程的⼊⼝函数. |
| start() | 通过调⽤ run() 开始执⾏线程。操作系统将根据优先级参数调度线程。如果线程已经在运⾏,这个函数什么也不做。 |
| currentThread() | 返回⼀个指向管理当前执⾏线程的 QThread的指针。 |
| isRunning() | 如果线程正在运⾏则返回true;否则返回false。 |
| sleep() / msleep() / usleep() | 使线程休眠,单位为秒 / 毫秒 / 微秒 |
| wait() | 阻塞线程,直到满⾜以下任何⼀个条件: 与此 QThread 对象关联的线程已经完成执⾏(即当它从run()返回时)。如果线程已经完成,这个函数将返回 true。如果线程尚未启动,它也返回 true。 已经过了⼏毫秒。如果时间是 ULONG_MAX(默认值),那么等待永远不会超时(线程必须从run()返回)。如果等待超时,此函数将返回 false。 这提供了与 POSIX pthread_join() 函数类似的功能。 |
| terminate() | 终⽌线程的执⾏。线程可以⽴即终⽌,也可以不⽴即终⽌,这取决于操作系统的调 度策略。在terminate() 之后使⽤ QThread::wait() 来确保。 |
| finished() | 当线程结束时会发出该信号,可以通过该信号来实现线程的清理⼯作。 |
3. 使用线程
定时器内部本质上也是可以居于多线程来实现的(Qt 的定时器是否基于多线程,不太清楚)
创建另一个线程,新线程中,进行计时(弄一个循环,每循环一次,sleep 1s,sleep 完成,就可以更新界面了)
创建一个新的类,继承自 QThread






- 第一步:

- 第二步:

- 第三步:

运行:

QThread 的应用场景:
之前学习的多线程,主要还是站在服务器开发的角度来看待的。
当时谈到的额多线程,最主要的目的,是为了充分利用多核 CPU 的计算资源。双路CPU(一个主板上面有两个 CPU)
客户端,多线程仍然非常有意义,侧重点不同了
对于普通用户来说,"使用体验"是一个非常重要的话题
如果"非常快"的代价是"系统很卡"用户大概率是不会买张的。虽然普通用户的家用 PC 上也是多核 CPU
相比之下,客户端中的多线程,主要适用于,通过多线程的方式,执行一些耗时的等待 IO 的操作,避免主线程被卡死,避免对用户造成一些不好的体验。
客户端经常会和服务器进行网络通信。比如客户端要上传/下载一个很大的文件,传输需要消耗很久。
上传/下载一个很大的文件:密集的IO操作,比如代码中持续不断的进行 QFile.write,这种密集 IO 就会使程序被系统阻塞,挂起,一旦进程都被挂起了,此时就意味着,用户进行的各种操作,程序都无法响应,例如,大型游戏需要更新的时候,需要从文件/网络加载大量的资源,此时如果你鼠标狂点窗口,很可能该窗口就会僵住,就会提示 "该窗口不能响应,是否要强制结束"。
因此,相比之下,更好的做法,使用单独的线程,来处理这种密集的 IO 操作,要挂起也是挂起这个新线程。主线程要负责事件循环,负责处理用户的各种操作,此时主线程仍然可以继续工作,继续响应用户的各种操作。
4. 线程安全
4.1 互斥锁
加锁:把多个线程要访问的公共资源,通过锁保护起来 --> 把并发执行变成串行执行。
Qt 中的锁,来针对系统提供的锁进行封装:QMutex,lock 加锁,unlock 解锁。






运行:

很明显结果是不对的。说明是存在线程安全问题的。进行加锁来解决。

多个线程进行加锁的对象,得是同一个对象,不同对象,此时就不会产生锁的互斥,也就无法把并发执行变为串行执行,也就无法解决上述问题。

num 是一个两个线程访问的公共变量,之前如果是并发执行,就可能第一个线程修改了一般,第二个线程也进行修改,就容易出现问题,因为 ++ 操作对应3个CPU指令。
加了锁之后,第一个线程顺利拿到锁,继续执行 ++,在第一个线程没执行完的时候,第二个线程只能阻塞等待,一直等第一个线程释放锁之后,第二个线程才能从阻塞中被唤醒。
再次执行:

上述代码很容易忘记释放锁。
lock
// ......
if(cond)
return;
// ......
unlock
出现以上情况也是没有释放锁,如果中间抛出异常,也是未释放锁。
C++ 中引入智能指针就是解决上述内存的释放问题的。
C++ 11 引入了 std::lock_guard, std::lock_guard相当于是 std::mutex 智能指针,借助 RAII 机制。
{
std::lock_guard(mutex);
// 执行各种逻辑......
} //大括号执行完毕,guard 变量的生命周期结束,就会在析构的时候,执行 unlock 了
上述方案,Qt 也参考过来了:QMutexLocker

运行:

Qt 的锁 和 C++ 标准库中的锁,本质上都是封装的系统提供的锁,编写多线程程序的时候,可以使用 Qt 的锁,也可以使用 C++ 的锁。C++ 的锁也是可以锁 Qt 的锁,虽然混着用也行,但是一般不建议。
4.2 条件变量
条件变量和信号量 和 Linux 中谈到的 条件变量/信号量 完全一致。
多个线程,之间的调度时无序的,为了能够一定程度的干预线程之间的执行顺序,引入条件变量。在 Qt 中,专门提供了 QWaitCondition类 来解决像上述这样的问题。
特点:QWaitCondition 是 Qt 框架提供的条件变量类,⽤于线程之间的消息通信和同步。
用途:在某个条件满⾜时等待或唤醒线程,⽤于线程的同步和协调。
cpp
QMutex mutex;
QWaitCondition condition;
//在等待线程中
mutex.lock();
// wait 中就会先释放锁 + 等待,要想释放锁,前提就是先获取到锁
//检查条件是否满⾜,若不满⾜则等待
while (!conditionFullfilled())
{
condition.wait(&mutex); //等待条件满⾜并释放锁
}
// 这里用的是while而不是if的原因是:唤醒之后,需要再确认一i西安,当前条件是否真的成立了,wait 可能被提前唤醒的(可能被信号打断了)
//条件满⾜后继续执⾏
//...
mutex.unlock();
//在改变条件的线程中
mutex.lock();
//改变条件
changeCondition();
condition.wakeAll(); //唤醒等待的线程
mutex.unlock();
4.3 信号量
课堂上谈到的信号量,其实还可以进行进程之间的控制。当然,也同样作为同一个进程内部的线程之间的通信方式。信号量也是计数器,描述了可用资源的个数。
特点:QSemaphore 是 Qt 框架提供的计数信号量类,⽤于控制同时访问共享资源的线程数量。
⽤途:限制并发线程数量,⽤于解决⼀些资源有限的问题。
cpp
QSemaphore semaphore(2); //同时允许两个线程访问共享资源
//在需要访问共享资源的线程中
semaphore.acquire(); //尝试获取信号量,若已满则阻塞
//访问共享资源
//...
semaphore.release(); //释放信号量
//在另⼀个线程中进⾏类似操作