Modern C++ Concurrency Utilities 04: Mutex, Lock and Concurrent Access Control
mutex
即 mutual exclusion, 表示一种互斥语义. 所谓互斥, 实际上是指所有权的唯一性, 即同一时刻一个互斥量只能由一个所有者持有. 在并发语境中, 互斥量这一原语的所有者一般是「线程」, 它的作用是用于阻止多个线程同时访问共享的资源, 从而避免 data race 并为多个线程的执行提供实现「同步」的机制.
std::mutex
标准库中的 std::mutex
类提供基本的互斥量原语, 表示不可重入 (non-recursive) 的互斥语义. 它提供基本的 lock
, try_lock
, unlock
等方法:
lock
: 一个线程可以通过调用lock
获取mutex
的所有权. 如果mutex
当前正被其他线程占有, 则lock
会阻塞try_lock
: 一个线程可以通过调用try_lock
尝试获取mutex
的所有权. 如果mutex
当前正被其他线程占用, 则try_lock
立刻返回false
.unlock
: 一个持有当前mutex
的线程可以通过调用unlock
释放mutex
.
需要注意的是:
- 如果当前线程正持有
mutex
, 那么再次对该mutex
调用lock
会造成死锁 (调用try_lock
不会死锁, 且返回false
, 但最好不要调用). mutex
的生命周期一定要比它的持有者更长.mutex
的持有者必须要在销毁前释放其对mutex
的所有权.- 为了避免以上几点得不到保证时程序出错, 最好不要对
mutex
手动调用lock
/try_lock
/unlock
等方法, 而是使用标准库提供的 RAII 管理类.
std::*_mutex
除基本的互斥量原语外, C++ 11 中也提供了具备更多功能的 *_mutex
类:
timed_mutex
: 除mutex
所具备的基本语义和操作外,timed_mutex
还提供了一种「半阻塞」获取mutex
所有权的方法:try_lock_for
: 尝试获取所有权, 在获取到所有权后返回true
或未获取到且阻塞了给定时间后返回false
try_lock_until
: 尝试获取所有权, 在获取到所有权后返回true
或未获取到且阻塞到给定的时间点后返回false
recursive_mutex
: 可重入锁/递归锁. 与基本的mutex
提供不可重入的互斥语义不同, 同一个线程可以多次对recursive_mutex
加锁.lock
获取recursive_mutex
的所有权,unlock
释放所有权. 不同的是同一个线程可以对同一个recursive_mutex
多次lock
/try_lock
, 并且当unlock
调用次数和之前lock
/try_lock
调用次数相等时该线程才正式释放该recursive_mutex
.recursive_mutex
的lock
次数可能是有上限的 (在实际中必然如此), 当达到上限后继续lock
会抛出std::system_error
异常.
recursive_timed_mutex
: 语义和recursive_mutex
相同, 多提供了try_lock_for
和try_lock_until
方法.
C++ 14 和 C++ 17 中为 mutex
拓展了「可共享」的语义:
-
shared_lock
: 可 共享锁, 或者可以理解为「读写锁」, 它允许两种语义的互斥:shared
: 多个线程可以共享shared_mutex
的所有权.exclusive
: 只有一个线程可以独占shared_mutex
的所有权.
这两种语义看似矛盾, 但其实它们的机制如下:
- 如果当前
shared_lock
已经被通过lock
/try_lock
获取了 独占所有权 , 那么任何其他的线程都无法获取该shared_lock
的任何所有权 - 如果当前
shared_lock
已经被通过lock_shared
/try_lock_shared
获取了 共享所有权, 那么其他线程无法获取它的独占所有权, 但是仍可以获取它的共享所有权.
shared_lock
适合于「读共享, 写独占」的场景和类似场景, 即多个线程可以安全地对共享资源同时进行一类操作 (如读取), 但是只能有一个线程进行另一类操作 (如写入).shared_lock
形式上的差别和语义上的差别对应, 它比mutex
多提供了用于共享所有权的方法:lock_shared
try_lock_shared
unlock_shared
-
shared_timed_mutex
相比shared_mutex
多提供了用于 timeout 的方法:try_lock_for
/try_lock_until
try_lock_shared_for
/try_lock_shared_until
Lock
C++ 中 mutex
表示一种原子变量, 它的状态改变操作是同步阻塞 (lock
) 或非阻塞 (try_lock
) 的. 而「锁」则是 mutex
的 RAII 风格管理类.
-
lock_guard
:lock_guard
是一个简单轻量的 RAII 风格的泛型 mutex wrapper. 它以一个mutex
风格 (提供了lock
和unlock
方法) 对象的 引用 为参数, 并持有这个引用, 且在构造函数中对该引用调用lock
, 在析构函数中调用unlock
. 仅此而已, 十分简单. -
scoped_lock
:scoped_lock
是 C++ 17 中对lock_guard
的一个拓展, 它支持对多个mutex
对象的 RAII 管理. 它的单个mutex
特化版本和lock_guard
相同, 因此只管理一个mutex
风格对象时scoped_lock
退化为lock_guard
, 因此可以说scoped_lock
无额外开销地替代了lock_guard
在管理多个 mutex 方面,
scoped_lock
使用std::lock
函数提供避免死锁的管理.scoped_lock
的实现也非常简单, 利用了一些 可变模板参数和折叠表达式 的特性. -
unique_lock
:unique_lock
与上面两种简单的 RAII 管理类有所不同, 它相当于一个可移动的 mutex wrapper, 并且支持延迟锁定 (即不在构造函数中立刻lock
). 它提供对所 wrap 的 mutex 对象的lock_*
/try_lock_*
/unlock
操作, 在构造函数中可以设置锁定策略:defer_lock
: 不在构造函数中调用lock
try_to_lock
: 在构造函数中调用try_lock
adopt_lock
: 假设当前线程已经获取了所传入 mutex 的所有权- 不设置锁定策略: 与
scoped_lock
/lock_guard
类似, 在构造函数中调用lock
unique_lock
可以defer_lock
并且可以多次lock
并unlock
的特点还使得它能够配合条件变量 (condition variable) 使用, 细节参考: -
shared_lock
:shared_lock
与unique_lock
角色类似, 不同之处在于它转为 共享所有权 设计, 因此只能用于shared_mutex
风格的 mutex 对象 (实现了lock_shared
等操作). 对于一个shared_lock
对象来说, 可以认为unique_lock<shared_lock>
wrap 了它的「独占所有权接口」, 而shared_lock<shared_lock>
wrap 了它的「共享所有权」接口.
others
标准库提供了两个死锁避免的加锁算法, 可以同时获取多把锁:
std::try_lock
: 对传入的所有 Lockable 对象依次调用try_lock
, 其中任意一个返回了false
都会终止后续的加锁并unlock
之前被获取的 lockable 对象, 且返回加锁失败的那个锁的下标 (在参数列表中的位置).std::lock
: 使用死锁避免算法对传入的 Lockable 对象加锁.scoped_lock
的多参数版本就使用了std::lock
.
对于简单的线程同步, 如控制一个过程在多线程环境中只调用一次, 标准库特地提供了 call_once
和 once_flag
:
1 | template<class Callable, class... Args> |
call_once
的语义是对于给定的 once_flag
和 Callable
对象以及参数, 保证只调用一次 f
. 其中 once_flag
应该是标志着 f
和 args
是否已被调用的状态标志.
References
- Concurrency support library (since C++11) - cppreference.com
- std::mutex - cppreference.com
- std::lock_guard - cppreference.com
- std::scoped_lock - cppreference.com
- std::unique_lock - cppreference.com
- std::shared_lock - cppreference.com
- c++ - std::unique_lockstd::mutex or std::lock_guardstd::mutex? - Stack Overflow
- c++ - What’s the best way to lock multiple std::mutex’es? - Stack Overflow
- c++ - std::lock_guard or std::scoped_lock? - Stack Overflow