1. 前言
std::mutex 也称为互斥量,C++ 11 与 mutex 相关的类和函数都声明在 #include<mutex> 头文件中
C++ 11 提供下面四种语义的互斥量:
1. std::mutex,独占的互斥量,不能递归使用
2. std:time_mutex,带超时的独占互斥量,不能递归使用
3. std::recursive_mutex,递归互斥量,不带超时功能
4. std::recursive_timed_mutex,带超时的递归互斥量
2. 独占互斥量和递归互斥量
std::mutex 是 C++11 最基本的互斥量,std::mutex 对象提供了独占所有权的特性,也就是不支持递归的对 std::mutex 上锁,但是 recursive_mutex 可以递归的对 互斥量对象上锁。
1. 构造函数,std::mutex 不允许拷贝构造,也不允许 move 拷贝,最初产生的 mutex 对象是处于 unlocked 状态的。
2. lock() 调用线程将锁住该互斥量,线程调用该函数会发生下面三种情况:
- 如果互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock 之前,该线程一直持有该锁;
- 如果当前的互斥量被其他线程锁住,则当前的调用线程被阻塞;
- 如果当前的互斥量被当前的线程锁住,则会产生死锁;
3. unlock 解锁,释放对互斥量的所有权
4.try_lock() 尝试锁住当前的互斥量,线程调用该函数会产生下面三种情况:
- 如果当前互斥量没有被其他线程锁住,则效果等同于 lock();
- 如果当前互斥量被其他线程锁住,则当前的调用返回 false,则并不会被阻塞掉;
- 如果当前互斥量被当前的线程锁住,则会产生死锁;
mutex 的示例代码如下:(如果 mMutex lock 两次会产生错误)
static void _threadLoopA(void) {
while (true) {
mMutex.lock();
//mMutex.lock();
for (int i = 0; i < 100; i++) {
cout << "_threadLoopA run loopindex" << i << endl;
}
mMutex.unlock();
this_thread::sleep_for(500ms);
}
}
static void _threadLoopB(void) {
while (true) {
mMutex.lock();
for (int i = 0; i < 100; i++) {
cout << "_threadLoopB run loopindex" << i << endl;
}
mMutex.unlock();
this_thread::sleep_for(500ms);
}
}
threadMutexDemo::threadMutexDemo() {
cout << "construct threadMutexDemo" << endl;
thread t1(_threadLoopA);
thread t2(_threadLoopB);
t1.detach();
t2.detach();
}
recursive_mutex 递归锁允许线程多次获取该锁,可以用来解决同一线程需要多次获取互斥量时死锁的问题。
recursive_mutex 的示例代码如下:注意 rmutex lock 上锁两次就必须解锁两次,否则 threadB 还是无法获取执行权限。
static void _threadLoopA(void) {
while (true) {
rmutex.lock();
rmutex.lock();
for (int i = 0; i < 100; i++) {
cout << "_threadLoopA run loopindex" << i << endl;
}
rmutex.unlock();
//rmutex.unlock();
this_thread::sleep_for(500ms);
}
}
static void _threadLoopB(void) {
while (true) {
rmutex.lock();
for (int i = 0; i < 100; i++) {
cout << "_threadLoopB run loopindex" << i << endl;
}
rmutex.unlock();
this_thread::sleep_for(500ms);
}
}
recursive_timed_mutex 和 time_mutex 多了两个超时获取锁的接口 try_lock_for 和 try_lock_until,这两个接口在超时时间内没有获取到锁会返回,超时时间内获取到锁也会返回,此时要对lock 的状态进行判断,如果不判断,则会强制执行锁下面的代码
3. lock_guard 和 unique_lock
使用 mutex 手动的 lock 和 unlock ,可能会在异常处理退出的时候忘记 unlock mutex,引发死锁问题,可以使用 C++ 提供的 基于 RAII(资源获取即初始化) 思想的模板 lock_guard<> 和 unique_lock <> 来实现更好的编码。
lock_guard 和 unique_lock 的区别
1. unique_lock 和 lock_guard 都能实现自动加锁和解锁,但是前者更加灵活,能实现更多的功能
2.unique_lock 可以进行临时上锁再加锁,如在构造对象之后使用 lck.unlock 就可以实现解锁,lck.lock 进行上锁,而不必等到析构的时候自动解锁,lock_guard 是不支持手动释放的。
3. 一般来说,使用 unique_lock 比较多,除非追求极致的性能才会使用 lokc_guard
static void _threadLoopA(void) {
while (true) {
lock_guard<mutex> lock(mMutex);
//unique_lock<mutex> lock(mMutex);
//rmutex.lock();
//rmutex.lock();
for (int i = 0; i < 100; i++) {
cout << "_threadLoopA run loopindex" << i << endl;
}
//rmutex.unlock();
//rmutex.unlock();
this_thread::sleep_for(500ms);
}
}
static void _threadLoopB(void) {
while (true) {
lock_guard<mutex> lock(mMutex);
//unique_lock<mutex> lock(mMutex);
//rmutex.lock();
for (int i = 0; i < 100; i++) {
cout << "_threadLoopB run loopindex" << i << endl;
}
//rmutex.unlock();
this_thread::sleep_for(500ms);
}
}
如果要使用条件变量,就一定要使用 unique_lock, 条件变量的目的就是为了:在没有获得某种提醒时长时时间休眠,需要一直处于休眠状态,条件变量使用 cond.wait() 休眠直到 cond_notity_one 唤醒才开始执行下一句,还有使用 cond_notify_all() 接口用于唤醒所有等待的线程。
条件变量在 wait 时会进行 unlock 再进入休眠,lock_guard 并没有此操作接口。
cond.wait : 如果线程被唤醒或者超时那么首先会进行lock 获取当前的锁,再判断条件(传入的参数)是否成立,如果成立wait 函数返回否则释放锁继续休眠
notify: 进行notify动作并不需要获取锁
使用场景:需要结合 noify + wait 的场景使用 unique_lock,如果当初的互斥使用 lock_guard()
4. atomic 相关
C++ 中的原子变量(Atomic Variable)通常用于多线程编程中,用于在并发环境下进行安全的共享数据操作。在多线程编程中,多个线程可能同时访问和修改同一个变量,如果没有正确的同步机制,可能会导致数据竞争和不一致性等问题,原子变量提供了一种简单有效的方式来解决这个问题。
原子变量一般在下面情况下使用:
- 共享数据访问,当多个线程需要访问和修改同一个共享变量时,可以使用原子变量来确保操作的原子性和线程安全。
- 避免数据竞争,原子变量可以避免数据竞争的问题,确保多个线程对共享变量的操作不会相互干扰,从而保证数据的一致性
- 无锁算法,原子变量通常和无锁算法结合(Lock-Free Algorithm),提供一种高效的并发数据结构的实现方式,避免了传统锁机制带来的性能开销和死锁问题
- 状态同步,在状态同步问题中,多个线程需要共同修改一个状态变量,使用原子变量可以确保状态变量的更新在多线程环境下是正确和同步的
STL提供了atomic 的头文件 来支持原子变量的使用,可以使用原子类型 atomic<int> 或者 atomic<bool>
4.1 原子变量和互斥锁的区别
原子变量和互斥锁都可以用于保护共享数据的安全,它们各自有不同的应用场景
使用原子变量的优点:
- 更加轻量级,原子变量通常比互斥锁更加轻量级,不需要线程之间的上下文切换和内核态和用户态的切换,从而减少了系统开销;
- 避免死锁,互斥锁可能会导致死锁问题,特别是在复杂的多线程场景中,原子变量不存在这个问题;
- 无锁算法,使用原子变量可以实现无锁算法,避免互斥锁带来的性能开销;
- 更简单,不需要手动的上锁和解锁操作;
使用互斥锁的情形:
- 复杂的临界区,如果多个线程需要对多个共享变量进行复杂操作,使用互斥锁可以更容易管理和控制临界区;
- 需要更加细粒度的控制,互斥锁可以提供更加细粒度的控制,允许锁定特定资源或者操作,原子变量一般用于单个变量的原子操作;
- 长时间占用资源的操作,如果临界资源执行的时间比较长,使用互斥锁更加合适,因为原子变量更适合短小的原子操作;
原子变量在简单的多线程场景中通常更高效和方便,而互斥锁适用于更复杂和需要更细粒度控制的多线程场景。