C++并发编程 3:互斥锁
互斥锁
std::mutex
C++里面的mutex类是用来进行线程同步,保护数据的,防止不同线程对同一数据同时进行处理。
std::mutex的成员函数:
构造函数
,std::mutex不允许拷贝构造,也不允许移动拷贝。lock()
,调用线程将锁住该互斥量。线程调用该函数会发生下面 3 种情况:- 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用unlock之前,该线程一直拥有该锁。
- 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。
- 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
try_lock()
,尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况,- 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用unlock释放互斥量。
- 如果当前互斥量被其他线程锁住,则当前调用线程返回false,而并不会被阻塞。
- 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
unlock()
, 解锁,释放对互斥量的所有权。
使用方法:
- 配合std::lock_guard,std::unique_lock或者std::scoped_lock使用。
std::timed_mutex
std::timed_mutex和std::mutex功能及用法基本一致,但多了两个函数:
try_lock_for(const std::chrono::duration<>& )
, 尝试在指定时间段之内锁住互斥量,如果超时则返回false。try_lock_until(const std::chrono::time_point<>& )
, 尝试在指定时间点之前锁住互斥量,如果超时则返回false。
读写锁
std::shared_mutex
C++17中引入std::shared_mutex
,读写锁同样用于实现共享和独占访问的互斥。
它提供了一种更加灵活的机制,允许多个线程在共享模式下读取数据,但只允许单个线程在独占模式下写入或修改数据。
与std::mutex相比,具有以下额外特性:
- 多个线程可以同时以共享模式(shared mode)持有锁,允许并发读取操作。
- 只有一个线程可以以独占模式(exclusive mode)持有锁,允许写入或修改操作。
- 当一个线程以共享模式持有锁时,其他线程可以以共享模式持有锁,允许并发读取操作。
- 当一个线程以独占模式持有锁时,其他线程无法以共享模式持有锁,它们必须等待独占模式的线程释放锁。
成员函数:
- lock:以独占模式锁定互斥,若互斥不可用则阻塞。
- try_lock:以独占模式尝试锁定互斥,若互斥不可用则返回。
- unlock:解锁互斥。
- lock_shared:以共享模式锁定互斥,若互斥不可用则阻塞。
- try_lock_shared:尝试以共享模式锁定互斥,若互斥不可用则返回。
- unlock_shared:解锁以共享模式锁定的互斥。
使用方法:
- 当使用共享模式时,配合std::shared_lock使用。
- 当使用独占模式时,配合std::lock_guard,std::unique_lock或者std::scoped_lock使用。
std::timed_shared_mutex
std::timed_shared_mutex和std::shared_mutex功能及用法基本一致,但多了4个函数:
try_lock_for(const std::chrono::duration<>& )
try_lock_until(const std::chrono::time_point<>& )
try_lock_shared_for(const std::chrono::duration<>& )
try_lock_shared_until(const std::chrono::time_point<>& )
递归锁
std::recursive_mutex
std::recursive_mutex与std::mutex一样,同样用于实现共享和独占访问的互斥,但是和std::mutex不同的是:
- std::recursive_mutex允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,
- std::recursive_mutex释放互斥量时需要调用与该锁lock()层次深度相同次数的unlock(),
- 除此之外,std::recursive_mutex的特性和std::mutex大致相同。
std::timed_recursive_mutex
std::timed_recursive_mutex和std::recursive_mutex功能及用法基本一致,但多了两个函数:
try_lock_for(const std::chrono::duration<>& )
try_lock_until(const std::chrono::time_point<>& )
使用场景
递归锁的主要使用场景是当一个线程需要多次锁定同一个互斥量时避免死锁。通常用于:
- 递归函数:一个函数在执行过程中需要获取一个锁,而它又会递归调用自身。
- 类内部的方法调用:一个类的成员函数需要获取锁,并且它内部又调用了另一个同样需要获取同一个锁的成员函数。
递归锁会带来额外的开销,而且它的使用往往意味着代码设计可能存在缺陷。
锁管理类
锁管理类用来简化互斥锁的使用,并确保锁的正确释放,即使发生异常。
锁管理类们利用了RAII思想,当锁管理类对象被创建时,它获取资源(比如锁定一个互斥量);当对象超出作用域被销毁时,它会自动释放资源(解锁互斥量)。
锁定策略
std::defer_lock、std::try_to_lock 和 std::adopt_lock 分别是空结构体标签类型std::defer_lock_t、 std::try_to_lock_t 和 std::adopt_lock_t的实例。 它们用于构造 std::lock_guard 、 std::unique_lock 及 std::shared_lock对象时指定锁定策略,控制互斥量的锁定行为。
- defer_lock_t:延迟锁定,不要在构造锁管理类对象时锁定互斥量。
- try_to_lock_t:尝试锁定互斥量。如果互斥量已经被其他线程锁定,构造函数不会阻塞,而是直接返回,并且锁管理类对象会处于未锁定状态。
- adopt_lock_t:假设互斥量已经被当前线程锁定。它不会尝试再次锁定互斥量,而是直接接管对已锁定互斥量的所有权。
std::lock_guard
std::lock_guard
类的构造函数:
- 禁用拷贝构造,禁用移动构造。
- explicit lock_guard( mutex_type& m )
- lock_guard( mutex_type& m, std::adopt_lock_t t)
std::lock_guard
除了构造函数和析构函数外没有其它成员函数。
{
std::lock_guard<std::mutex> lock(queueMutex);
// 可省略模板参数列表(C++17),等价于
std::lock_guard lock(queueMutex);
}
std::unique_lock
std::unique_lock
比std::lock_guard
更灵活:
- lock_guard在构造时或者构造前就已经获取互斥锁,并且在作用域内保持获取锁的状态,直到作用域结束;unique_lock在构造时或者构造后获取锁,在作用域范围内可以手动获取锁和释放锁,作用域结束时如果已经获取锁则自动释放锁;
- lock_guard锁的持有只能在lock_guard对象的作用域范围内,作用域范围之外锁被释放;而unique_lock对象支持移动操作,可以将unique_lock对象通过函数返回值返回,这样锁就转移到外部unique_lock对象中,延长锁的持有时间;
灵活的代价就是性能的损失:std::unique_lock
性能和内存开销都比std::lock_guard
大得多。
- unique_lock(): 默认构造
- unique_lock( unique_lock&& other ):支持移动构造,不支持拷贝构造
- explicit unique_lock( mutex_type& m )
- unique_lock( mutex_type& m, std::defer_lock_t t )
- unique_lock( mutex_type& m, std::try_to_lock_t t )
- unique_lock( mutex_type& m, std::adopt_lock_t t )
- unique_lock( mutex_type& m, const std::chrono::duration<>& )
- unique_lock( mutex_type& m, const std::chrono::time_point<>& )
std::unique_lock
的成员函数:
-
lock()
try_lock()
try_lock_for()
try_lock_util()
owns_lock()
:返回当前 std::unique_lock 对象是否获得了锁- operator bool(): 同
owns_lock()
mutex()
:返回当前 std::unique_lock 对象所管理的 Mutex 对象的指针release()
:返回所管理的mutex对象的指针,并释放锁对象对mutex的所有权,但不改变mutex的状态,unlock()
:解锁互斥量,但锁对象仍拥有对mutex的所有权。
std::scoped_lock
std::scoped_lock允许一次性锁住多个互斥量,并且在scoped_lock的生命周期结束时自动解锁。
构造函数:
- explicit scoped_lock( MutexTypes&… m )
- scoped_lock( std::adopt_lock_t, MutexTypes&… m )
- scoped_lock( const scoped_lock& ) = delete:不支持拷贝构造和移动构造
std::scoped_lock除了构造函数和析构函数外,没有其他成员函数。
std::shared_lock
shared_lock是共享互斥所有权包装器(unique_lock则是独占互斥所有权包装器)。
一般使用std::shared_lock<std::shared_mutex>
来进行读操作,使用 std::unique_lock<std::shared_mutex>
来进行写或修改操作。
- 当一个线程获取了std::unique_lock时,其他任何线程(包括试图获取 shared_lock 和 unique_lock 的线程)都会被阻塞。
- 当多个线程获取了std::shared_lock时,它们可以同时执行临界区代码,但如果此时有线程试图获取std::unique_lock,它会被阻塞,直到所有shared_lock都被释放。
构造函数和成员函数与std::unique_lock
类似,详见链接。
call once
call_once保证可调用对象f只被执行一次,即使同时从多个线程调用。
相比使用std::mutex, call_once的开销更低。
void call_once(std::once_flag& flag, Callable&& f, Args&&... args);
- flag:标志对象,用于指示f是否已调用过。
- f:可调用对象。
- args:传递给f的参数
#include <iostream>
#include <thread>
#include <mutex>
std::once_flag flag;
void init()
{
std::call_once(flag, [](){ std::cout << "init done.\n"; });
}
int main()
{
std::thread t1(init);
std::thread t2(init);
t1.join();
t2.join();
}
output:
Simple example: called once
提高锁的性能
- 精细化锁的粒度:将大范围的锁分解为多个小范围的锁,减少锁竞争。
- 在最短时间内持锁:持锁期间避免任何耗时操作。
- 选择合适的锁:对于读多写少的场景,使用读写锁。
如何避免死锁
- 使用RAII的锁管理类,避免忘记解锁造成的死锁。
- 确保所有线程按照相同的顺序请求和释放资源。
- 在执行需要多个资源的操作之前,一次性获取所有需要的锁(std::lock或std::scoped_lock)
- 避免嵌套锁(在一个锁的作用域内获取另一个锁)