目录
初识Qt多线程
QThread常用API
QThread的使用
Qt中的锁
条件变量和信号量
初识Qt多线程
Qt 多线程 和 Linux 中的线程本质是一个东西
Linux 中学过的 多线程 APl,Linux 系统提供的 pthread 库
Qt 中针对系统提供的线程 API 重新封装了
C++11 中,也引入了线程 std:thread
Qt API 设计的时候,博采众家之长:
- Linux 原生的多线程 API,设计的非常差,使用起来非常不方便的(也是C语言本身的局限性引起的)
实际开发中,很少会使用原生 api - C++ 的 std::thread 要比 Linux 的 API 要好一些
- Qt 中的多线程 API,还要更好一点,其实参考了 Java 中的线程库 API 的设计方式
在 Qt 中,多线程的处理一般是通过 QThread类 来实现
QThread 代表一个在应用程序中可以独立控制的线程,也可以和进程中的其他线程共享数据
OThread 对象管理程序中的一个控制线程
QThread要想创建线程,就需要创建出这样类的实例
创建线程的时候,需要重点指定线程的入口函数
Qt中,使用方式就是创建一个 QThread 的子类,重写其中的 run 函数,起到指定入口函数的方式(多态)
QThread常用API
QThread的使用
之前基于定时器,写过倒计时这样的程序,也可以通过线程,来完成类似的功能
定时器内部本质上也是可以基于多线程来实现的(Qt 的定时器是否基于多线程,不太清楚)
创建另一个线程,新线程中进行计时(搞一个循环,每循环一次,sleep 1s,sleep 完成,就可以更新界面了)
首先图形化界面的方式拖动一个 LCD Number,初始值设为10:
接下来创建一个类 Thread,继承自 QThread:
由于存在线程安全问题,多个线程同时对于界面的状态进行修改,此时就会导致界面就出错了
Qt 选择了 一刀切,针对界面的控件状态进行任何修改,务必在主线程中执行
在 thread.h 中,声明需要重写的 run 函数,再声明一个信号:
run 函数的实现:
接着在 widget.h 中创建一个 Thread 对象 thread,和一个处理信号的槽函数 handle:
Widget 构造函数如下:
槽函数 handle:
此时运行程序,就完成了倒计时的功能:
下面总结一下执行流程:
- 主线程启动新线程
- 新线程每隔一秒发送一次信号
- 主线程收到信号后,调用槽函数修改倒计时的值
Qt中的锁
说起多线程,最需要注意的就是线程安全问题,因为多线程程序太复杂了
而解决线程安全问题,最主要的措施就是加锁
把多个线程要访问的公共资源,通过锁保护起来 => 把并发执行变成串行执行
关于锁,Linux、C++、Qt 都有一套规定:
- Linux:mutex 互斥量
- C++11 引入 std:.mutex
- Qt 同样也提供了对应的锁 QMutex,来针对系统提供的锁进行封装
与 C++ 的 std:.mutex 相差不大,lock 加锁,unlock 解锁
下面演示 Qt 中锁的使用
创建好项目后,同样创建一个新的类 Thread,继承自 QThread:
因为下面想要两个线程对同一个变量进行操作,所以在 Thread 中添加一个静态成员 num,并声明重写的 run 方法:
注意 num 需要类内声明,类外初始化(在 thread.cpp 中初始化:int Thread::num = 0;)
run 方法就是循环5万次,每次给 num++:
下面在 widget.cpp 中编写构造函数的逻辑:
此时打印的结果如下:
并不是我们预期的10万,说明是存在 bug,说明是存在线程安全问题的
所以需要加锁处理,为了让两个线程用同一把锁,就将这个锁设为 static 的:
同样需要在 thread.cpp 中定义:QMutex Thread::mutex;
有了锁 mutex 后,就可以在 run 函数中 num++ 前后进行加锁和解锁操作:
此时再次运行程序,num 的值就是我们期望的10万了:
上面虽然结果正确了,但是这里的锁很容易忘记释放,忘记 unlock
实际开发中,加锁之后涉及到的逻辑可能很复杂,下面很容易就忘记释放锁
在 C++ 中释放动态内存也存在类似的问题,在释放前如果有 return 或 抛异常,就会出问题
C++ 在内存这里采用智能指针进行处理,而锁的释放,C++11 引入了 std::lock_guard,就相当于是 std::mutex 的智能指针,也是借助 RAII 机制
所以 std::lock_guard 一般如下所示:
{std::lock_guard(mutex);// 执行各种逻辑
}
大括号执行完毕, guard 变量的生命周期结束,就会在析构的时候,执行 unlock 了
上述方案,Qt也参考过来了,即 QMutexLocker
所以上述的 run 函数改为:
此时每次循环结束,locker 就会自动解锁,每次进入循环又会自动加锁,不会出现忘记解锁,或是有 return 和 异常 跳过解锁的情况
运行程序,依旧能够执行出正确的结果:
注意:
Qt 的锁 和 C++ 标准库中的锁,本质上都是封装的系统提供的锁
编写多线程程序的时候,可以使用 Qt 的锁,也可以使用 C++的锁,C++的锁也是可以锁 Qt 的线程的(虽然混着用也行,一般不建议)
条件变量和信号量
这里的条件变量和信号量,和 Linux 中谈到的条件变量/信号量完全一致
条件变量
多个线程之间的调度是无序的,为了能够一定程度的干预线程之间的执行顺序,引入条件变量
QWaitCondition
- wait
- wake
- wakeAll
要想使用条件变量,首先要进行加锁,因为在 wait 中就会进行 释放锁 + 等待
要想释放锁, 前提就是先获取到锁
并且还要搭配 while 循环,判断条件是否成立:
// 判定线程继续执行的条件是否成立,不成立就进行 wait 等待
while(!conditionFullfilled())
{condition.wait(&mutex);//等待条件满足并释放锁
}
这里要使用while 判定而不是 if,是因为:
唤醒之后,需要再确认一下当前条件是否真的成立了,wait 可能被提前唤醒的(可能被信号打断了)
信号量
这里谈到的信号量,其实还可以进行进程之间的控制
当然,也同样可以作为同一个进程内部的线程之间的通信方式
信号量其实就是计数器,描述了可用资源的个数
使用示例:
// 同时允许两个线程访问共享资源
QSemaphore semphore(2);
// 在需要访问共享资源的线程中
semaphore.acquire(); // 尝试获取信号量,若已空则阻塞(P)
// 访问共享资源
....
semaphore.release(); // 释放信号量(V)
// 在另一个线程中进行类似操作
Qt:多线程相关到此结束