这篇文章写于2021/08/05
主要用来记录一下C++多线程的知识,包括基础理论部分,以及std中的thread以及mutex等使用方法。
首先是基础知识
1、什么是进程,什么是线程?
- 进程是对运行时程序的封装,是系统进行资源调度和分配的基本单位,实现了操作系统的并发。
- 线程是进程的子任务,是CPU调度和分派的基本单位,用于保证程序的实时性,实现进程内部的并发,线程是操作系统可识别的最小执行和调度单位。
2、进程间的通信方式
- 进程间通信主要包括管道、系统IPC(消息队列、信号量、信号、共享内存)、套接字socket。
- 管道
- 普通管道PIPE
- 它是半双工的 (数据只能在一个方向上流动),具有固定的读端和写端
- 它只能用于具有亲缘关系的进程之间通信,也就是父子进程或者兄弟进程
- 它可以看成是一种特殊的文件,读写可以用read、write等函数
- 命名管道FIFO
- 它可以在无关的进程之间交换数据
- 它有路径名与之关联,以一种特殊设备文件的形式存在于文件统中
- 系统IPC
- 消息队列
- 消息队列是消息的连接表,存放在内核中。一个消息队列由一个标识符来标记。具有写权限的进程可以按照一定的规则向消息队列中添加新信息。对消息队列有读权限的进程则可以从消息队列中读取信息。
- 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级
- 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容不会删除
- 消息队列额可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
- 信号量 Semaphore
- 信号量与已经介绍过的IPC结构不同,它是一个计数器,可以用来控制多个进程对共享数据的访问。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据
- 信号量用于进程间同步,若要在进程间传递数据,需要结合共享内存
- 信号量基于操作系统的PV操作,程序对信号量的操作都是原子操作
- 每次对信号量的PV操作不仅限于对信号量值加1减1,而且可以加减任意正整数
- 支持信号量组
- 信号 Signal
- 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生
- 共享内存 Shared memory
- 它使得多个进程可以访问同一块内存空间,不同进程可以及时的看到对方进程中对共享内存的数据进行更新。这种方式需要依赖某种同步操作,如互斥锁跟信号量等
- 共享内存是最快的一种IPC,因为进程是直接对内存进行读取
- 因为多个进程可以同时操作,所以需要进行同步
- 信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问
- 套接字
- socket也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同主机之间的进程通信
3、线程间的通信方式
- 临界区
- 通过多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问
- 互斥量Synchronized/Lock
- 采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可保证公共资源不会被多个线程同时访问
- 信号量 Semphare
- 为控制具有有限数量的用户资源设计的,它允许多个线程在同一时刻去访问同一个资源,但一般需要限制同一时刻访问此资源的最大线程数量
- 事件/信号 Wait/Notify
- 通过通知操作的方式来保持多线程同步,还饿可以方便的实现多线程优先级的操作
4、单核机器上写多线程程序,是否需要考虑加锁,为什么?
- 需要,只要是多线程程序,就会需要同步和通信,这跟是否单核多核没关系
- 单核和多核都能实现多线程,单核主要靠轮换来并发,多核可以做到真正意义上的并发
5、死锁发生的条件以及如何避免死锁?
- 死锁是指两个或两个以上进程在执行过程中,因争夺资源而造成的相互等待的现象。死锁发生条件如下
- 互斥条件
- 进程对所分配到的资源 不允许其他进程访问,若其他进程访问该资源,只能等待,直到占有该资源的进程使用完后释放该资源
- 请求和保持条件
- 进程获得一定资源后,又对其他资源发出请求,但是该资源可能被其他进程占有,此时请求阻塞,但该进程不会释放自己已经占有的资源
- 不可剥夺条件
- 进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用后自己释放
- 环路等待条件
- 进程发生死锁后,必然存在一个进程-资源之间的环形链
- 解决死锁的办法即破坏上述四个条件之一,主要方法如下
- 资源一次性分配
- 可剥夺资源
- 当进程新的资源未得到满足时,释放已占有资源,从而破坏不可剥夺的条件
- 资源有序分配法
- 给所有资源赋予序号,进程按照编号递增请求资源,释放则相反,从而破坏环路等待条件
6、父进程退出时,子进程会如何?父线程退出时,子线程会如何?
- 父进程退出时,子进程会过继给systemd
- 父线程退出时,子线程不一定退出,只有父进程退出时,子线程才退出
std中的thread相关
- 创造一个线程并令该线程执行f函数
std::thread t(f, arg1, arg2, ...); - 令父线程先停止做事,让子线程先做
t.join(); - 分离线程,让该线程一直做下去,直到进程结束
t.detach();
线程间共享数据 (主要是锁)
- 定义申明一个互斥锁
std::mutex mtx; - 锁上一个能自动解锁的互斥锁
std::lock_guard<std::mutex> lock(mtx); - 能够同时给多个互斥锁上锁
std::lock(mtx1, mtx2); - 获取能自动解锁的互斥锁权限
std::lock_guard<std::mutex> lock(mtx, std::adopt_lock); - 申明一个层次锁,每次只能获取更低编号的
hierarchical_mutex high_level_mutex(10000); - 让互斥锁保持未上锁状态
std::unique_lock<std::mutex> lock(mtx, std::defer_lock); - 类似于读写锁
std::shared_mutex shared_mtx; - 提供多线程读取
std::shared_lock<std::shared_mutex> shared_lock(shared_mtx); - 提供独占写访问,此时读线程也被阻塞
std::lock_guard<std::shard_mutex> lock(shared_mtx); unique_lock 和lock_guard 区别?
unique_lock 比lock_guard 更具有弹性,unique_lock 不一定要拥有mutex。可以创建出空的对象,unique_lock 可以转移,可以函数回传。unique_lock 提供lock 、unlock 、try_lock 成员函数。
同步并发操作 (条件变量)
- 等待线程在检查的时候休眠
std::this_thread::sleep_for(std::chrono::milliseconds(100)); - 条件变量的创建和使用
- 头文件
#include <condition_variable> - 创建一个条件变量
std::condition_variable data_cond data_cond; - 通知等待线程
data_cond.notify_one(); - 当执行到此或者被notify的时候触发。如果有第二参数,先判断第二参数return的值,如果是false,则开锁并阻塞直到被notify重新验证;如果是true,关锁继续执行语句。当没有第二参数时候,执行到此等同于第二参数为false,被notify则直接等同于第二参数为true
data_cond.wait(u_lock, []{ return cond; }); - 生产者消费者例子
#include <iostream>
#include <condition_variable>
#include <thread>
#include <mutex>
#include <queue>
std::mutex mtx;
std::queue<int> q;
std::condition_variable cond;
int a = 1;
void preparation() {
for (int i = 0; i < 20000; ++i) {
{
std::lock_guard<std::mutex> g_lock(mtx);
q.push(i);
std::cout << i << " has been prepared." << std::endl;
}
cond.notify_one();
}
}
void processing() {
while (true) {
std::unique_lock<std::mutex> u_lock(mtx);
cond.wait(u_lock, []{ return !q.empty(); });
int front = q.front();
std::cout << front << " has been processing." << std::endl;
q.pop();
u_lock.unlock();
if (front == 19999) break;
}
}
int main() {
std::thread t1(preparation);
std::thread t2(processing);
t1.join();
t2.join();
return 0;
}
|