简介
本文介绍C++ 并发中使用的其他类型的锁,包括unique_lock
,shared_lock
, 以及recursive_lock
等。shared_lock
和unique_lock
比较常用,而recursive_lock
用的不多,或尽可能规避用这种锁。
unique_lock
unique_lock
和lock_guard
基本用法相同,构造时默认加锁,析构时默认解锁,但unique_lock
有个好处就是可以手动解锁。这一点尤为重要,方便我们控制锁住区域的粒度(加锁的范围大小),也能支持和条件变量配套使用,至于条件变量我们之后再介绍,本文主要介绍锁的相关操作。
1 | //unique_lock 基本用法 |
我们可以通过unique_lock
的owns_lock
判断是否持有锁
1 | //可判断是否占有锁 |
上述代码输出
1 | owns lock |
unique_lock
可以延迟加锁
1 | //可以延迟加锁 |
那我们写一段代码综合运用owns_lock
和defer_lock
1 | //同时使用owns和defer |
上述代码回依次输出, 但是程序会阻塞,因为子线程会卡在加锁的逻辑上,因为主线程未释放锁,而主线程又等待子线程退出,导致整个程序卡住。
1 | Main thread has the lock. |
和lock_guard
一样,unique_lock
也支持领养锁
1 | //同样支持领养操作 |
尽管是领养的,但是打印还是会出现owns lock
,因为不管如何锁被加上,就会输出owns lock
。
既然unique_lock
支持领养操作也支持延迟加锁,那么可以用两种方式实现前文lock_guard
实现的swap
操作。
1 | //之前的交换代码可以可以用如下方式等价实现 |
大家注意一旦mutex
被unique_lock
管理,加锁和释放的操作就交给unique_lock
,不能调用mutex
加锁和解锁,因为锁的使用权已经交给unique_lock
了。
我们知道mutex
是不支持移动和拷贝的,但是unique_lock
支持移动,当一个mutex
被转移给unique_lock
后,可以通过unique_ptr转移其归属权.
1 | //转移互斥量所有权 |
锁的粒度表示加锁的精细程度,一个锁的粒度要足够大,保证可以锁住要访问的共享数据。
同时一个锁的粒度要足够小,保证非共享数据不被锁住影响性能。
而unique_ptr
则很好的支持手动解锁。
1 | void precision_lock() { |
共享锁
试想这样一个场景,对于一个DNS服务,我们可以根据域名查询服务对应的ip地址,它很久才更新一次,比如新增记录,删除记录或者更新记录等。平时大部分时间都是提供给外部查询,对于查询操作,即使多个线程并发查询不加锁也不会有问题,但是当有线程修改DNS服务的ip记录或者增减记录时,其他线程不能查询,需等待修改完再查询。或者等待查询完,线程才能修改。也就是说读操作并不是互斥的,同一时间可以有多个线程同时读,但是写和读是互斥的,写与写是互斥的,简而言之,写操作需要独占锁。而读操作需要共享锁。
要想使用共享锁,需使用共享互斥量std::shared_mutex
,std::shared_mutex
是C++17标准提出的。
C++14标准可以使用std::shared_time_mutex
,
std::shared_mutex
和 std::shared_timed_mutex
都是用于实现多线程并发访问共享数据的互斥锁,但它们之间存在一些区别:
std::shared_mutex
:
* 提供了 `lock()`, `try_lock()`, 和 `try_lock_for()` 以及 `try_lock_until()` 函数,这些函数都可以用于获取互斥锁。
* 提供了 `try_lock_shared()` 和 `lock_shared()` 函数,这些函数可以用于获取共享锁。
* 当 `std::shared_mutex` 被锁定后,其他尝试获取该锁的线程将会被阻塞,直到该锁被解锁。
std::shared_timed_mutex
:
* 与 `std::shared_mutex` 类似,也提供了 `lock()`, `try_lock()`, 和 `try_lock_for()` 以及 `try_lock_until()` 函数用于获取互斥锁。
* 与 `std::shared_mutex` 不同的是,它还提供了 `try_lock_shared()` 和 `lock_shared()` 函数用于获取共享锁,这些函数在尝试获取共享锁时具有超时机制。
* 当 `std::shared_timed_mutex` 被锁定后,其他尝试获取该锁的线程将会被阻塞,直到该锁被解锁,这与 `std::shared_mutex` 相同。然而,当尝试获取共享锁时,如果不能立即获得锁,`std::shared_timed_mutex` 会设置一个超时,超时过后如果仍然没有获取到锁,则操作将返回失败。
因此,std::shared_timed_mutex
提供了额外的超时机制,这使得它在某些情况下更适合于需要处理超时的并发控制。然而,如果不需要超时机制,可以使用更简单的 std::shared_mutex
。
C++11标准没有共享互斥量,可以使用boost提供的boost::shared_mutex
。
如果我们想构造共享锁,可以使用std::shared_lock
,如果我们想构造独占锁, 可以使用std::lock_gurad
.
我们用一个类DNService
代表DNS服务,查询操作使用共享锁,而写操作使用独占锁,可以是如下方式的。
1 | class DNService { |
QueryDNS
用来查询dns信息,多个线程可同时访问。AddDNSInfo
用来添加dns信息,属独占锁,同一时刻只有一个线程在修改。
递归锁
有时候我们在实现接口的时候内部加锁,接口内部调用完结束自动解锁。会出现一个接口调用另一个接口的情况,如果用普通的std::mutex
就会出现卡死,因为嵌套加锁导致卡死。但是我们可以使用递归锁。
但我个人并不推荐递归锁,可以从设计源头规避嵌套加锁的情况,我们可以将接口相同的功能抽象出来,统一加锁。下面的设计演示了如何使用递归锁
1 | class RecursiveDemo { |
我们可以看到AddScore
函数内部调用了QueryStudent
, 所以采用了递归锁。
但是我们同样可以改变设计,将两者公有的部分抽离出来生成一个新的接口AddScoreAtomic
.
AddScoreAtomic
可以不适用递归锁,照样能完成线程安全操作的目的。
总结
本文介绍了unique_lock,共享锁,递归锁等的使用,较为全面的介绍了这几种锁的使用场景和潜在风险。
视频链接
https://space.bilibili.com/271469206/channel/collectiondetail?sid=1623290
源码链接