c++多线程基础二 -- 锁
1 线程锁类型
线程之间的锁有:互斥锁、条件锁、自旋锁、读写锁、递归锁。一般而言,锁的功能越强大,性能就会越低。
1.1 互斥锁
互斥锁用于控制多个线程对他们之间共享资源互斥访问的一个信号量。也就是说是为了避免多个线程在某一时刻同时操作一个共享资源。例如线程池中的有多个空闲线程和一个任务队列。任何是一个线程都要使用互斥锁互斥访问任务队列,以避免多个线程同时访问任务队列以发生错乱。
在某一时刻,只有一个线程可以获取互斥锁,在释放互斥锁之前其他线程都不能获取该互斥锁。如果其他线程想要获取这个互斥锁,那么这个线程只能以阻塞方式进行等待。
使用 :std::mutex
1.2 条件锁
条件锁就是所谓的条件变量,某一个线程因为某个条件为满足时可以使用条件变量使改程序处于阻塞状态。一旦条件满足以“信号量”的方式唤醒一个因为该条件而被阻塞的线程。最为常见就是在线程池中,起初没有任务时任务队列为空,此时线程池中的线程因为“任务队列为空”这个条件处于阻塞状态。一旦有任务进来,就会以信号量的方式唤醒一个线程来处理这个任务。
使用 :pthread_cond_t
1.3 自旋锁
个线程想要获取一个被使用的自旋锁,那么它会一致占用CPU请求这个自旋锁使得CPU不能去做其他的事情,直到获取这个锁为止,这就是“自旋”的含义。
使用 :spinlock_t
1.4 读写锁
写者:写者使用写锁,如果当前没有读者,也没有其他写者,写者立即获得写锁;
否则写者将等待,直到没有读者和写者。
读者:读者使用读锁,如果当前没有写者,读者立即获得读锁;否则读者等待,直到没有写者。
操作 | 函数说明 |
---|---|
初始化读写锁 | pthread_rwlock_init 语法 |
读取读写锁中的锁 | pthread_rwlock_rdlock 语法 |
读取非阻塞读写锁中的锁 | pthread_rwlock_tryrdlock 语法 |
写入读写锁中的锁 | pthread_rwlock_wrlock 语法 |
写入非阻塞读写锁中的锁 | pthread_rwlock_trywrlock 语法 |
解除锁定读写锁 | pthread_rwlock_unlock 语法 |
销毁读写锁 | pthread_rwlock_destroy 语法 |
c++17 引入std::shared_mutex
, 底层实现时操作系统提供的读写锁,也就是说,再有多个线程对共享资源读且少许线程对共享资源写的情况下, std::shared_mutex
比 std::mutex
效率更高。
std::shared_mutex
提供了lock
方法和 unlock
方法分别用于获取写锁和解除写锁,提供了lock_shared
方法和unlock_shared
方法分别用于获取读锁和解除读锁。一般将写锁模式称为排它锁( Exclusive Locking),将读锁模式成为共享锁(Shared Locking)。
std::unique_lock
对std::shared_mutex
写锁管理std::shared_lock
对std::shared_mutex
读锁管理
1.5 递归锁
所谓递归锁,就是在同一线程上该锁是可重入的,对于不同线程则相当于普通的互斥锁。
使用规则:例如函数a需要获取锁mutex,函数b也需要获取锁mutex,同时函数a中还会调用函数b。如果使用std::mutex必然会造成死锁。但是使用std::recursive_mutex就可以解决这个问题。
使用 :std::recursive_mutex
2 锁的使用
2.1 基本使用:
- 互斥锁:lock和unlock要对应,特别注意处理异常处理时,需要unlock释放锁
- 多个线程锁:要保证多个互斥量上锁的顺序一样就不会造成死锁。即嵌套多个线程锁,要保证上锁一致
- lock_guard类模板:构造函数进行加锁,析构函数解锁,采用作用域形式,保证加锁和解锁对应
2.2 死锁
死锁至少有两个互斥量mutex1,mutex2。
2.2.1 死锁产生的四个必要条件
- 互斥条件:资源是独占的且排他使用,进程互斥使用资源,即任意时刻一个资源只能给一个进程使用,其他进程若申请一个资源,而该资源被另一进程占有时,则申请者等待直到资源被占有者释放。
- 不可剥夺条件:进程所获得的资源在未使用完毕之前,不被其他进程强行剥夺,而只能由获得该资源的进程资源释放。
- 请求和保持条件:进程每次申请它所需要的一部分资源,在申请新的资源的同时,继续占用已分配到的资源。
- 循环等待条件:在发生死锁时必然存在一个进程等待队列{P1,P2,…,Pn},其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个进程等待环路,环路中每一个进程所占有的资源同时被另一个申请,也就是前一个进程占有后一个进程所深情地资源。
以上给出了导致死锁的四个必要条件,只要系统发生死锁则以上四个条件至少有一个成立。事实上循环等待的成立蕴含了前三个条件的成立,似乎没有必要列出然而考虑这些条件对死锁的预防是有利的,因为可以通过破坏四个条件中的任何一个来预防死锁的发生。
常规做法:
-
保证多个互斥量上锁的顺序一样就不会造成死锁。
-
std::lock()函数模板:
- std::lock(mutex1,mutex2……); 一次锁定多个互斥量(一般这种情况很少),用于处理多个互斥量。
- 如果互斥量中一个没锁住,它就等着,等所有互斥量都锁住,才能继续执行。如果有一个没锁住,就会把已经锁住的释放掉(要么互斥量都锁住,要么都没锁住,防止死锁)
- 使用std::lock,需要自己手动unlock 互斥锁
-
std::lock_guard的std::adopt_lock参数
- std::lock_guard<std::mutex> my_guard(my_mutex,std::adopt_lock);
加入adopt_lock后,在调用lock_guard的构造函数时,不再进行lock(); - std::lock 配合 lock_guard 和std::adopt_lock 不用手动unlock
- std::lock_guard<std::mutex> my_guard(my_mutex,std::adopt_lock);
3 锁的管理
互斥量管理 | 版本 | 作用 |
---|---|---|
lock_guard | C++11 | 基于作用域的互斥量管理 |
unique_lock | C++11 | 更加灵活的互斥量管理 |
shared_lock | C++14 | 共享互斥量管理 |
scoped_lock | C++17 | 互斥量避免死锁的管理 |
2.3 lock_guard 和 unique_lock 区别
unique_lock 继承 lock_guard 功能,但是比lock_guard更加灵活,可以降低锁的粒度。重点介绍unique_lock功能
2.3.1 第二个参数
-
std::adopt_lock
- 表示这个互斥量已经被lock(),即不需要在构造函数中lock这个互斥量了。
- 前提:必须提前lock
- lock_guard中也可以用这个参数
-
std::try_to_lock
- 尝试用mutx的lock()去锁定这个mutex,但如果没有锁定成功,会立即返回,不会阻塞在那里;
- 使用try_to_lock的原因是防止其他的线程锁定mutex太长时间,导致本线程一直阻塞在lock这个地方
- 前提:不能提前lock();
- owns_locks()方法判断是否拿到锁,如拿到返回true
-
std::defer_lock
- 如果没有第二个参数就对mutex进行加锁,加上defer_lock是始化了一个没有加锁的mutex
- 不给它加锁的目的是以后可以调用unique_lock的一些方法
- 前提:不能提前lock
2.3.2 降低锁粒度
配合 std::defer_lock 使用,可以实时unlock。
2.3.3 unique_lock所有权的传递
使用move或者临时unique_lock变量返回可以传递unique_lock的所有权
2.4 锁传递
c++ 中mutex锁没有复制构造函数,因此不能直接复制调用;需采用指针传递,这里推介使用结合智能指针使用
// 定义
std::shared_ptr<std::mutex> mtx_ptr;
// 初始化
mtx_ptr = make_shared<std::mutex>();
// 使用
std::lock_guard<std::mutex> lock(*mtx_ptr);
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!