在 C++ 多线程编程中,我们绕不开一个核心问题:共享资源竞争。为了保护数据一致性,锁是最常用的工具:访问共享资源前 lock 加锁,操作完成后 unlock 解锁,使用起来虽然简单,但存在一些潜在的风险,例如:
- 分支遗漏:如代码中
if条件触发提前返回,直接跳过unlock - 异常中断:操作共享资源时若抛出异常,程序会直接跳转至异常处理,unlock 无法被调用
- 人为疏忽:开发者在复杂逻辑中忘记编写
unlock语句
// 1. 异常中断
std::mutex mtx;
void demo01()
{
mtx.lock();
throw std::runtime_error("发生异常!");
mtx.unlock();
}
// 2. 分支遗漏
void demo02()
{
mtx.lock();
if (true)
{
return;
}
mtx.unlock();
}
// 3. 人为疏忽
void demo03()
{
mtx.lock();
// 忘记写 unlock
}
显然,通过手动管理保证锁的释放,风险过高。我们需要一种让锁自动管理的机制,从根源上规避这类问题。
C++ 提供了一种基于 RAII 原则的资源管理机制,用于对各种资源(如内存、文件、锁等)进行自动管理。
其中,std::lock_guard、std::unique_lock、std::shared_lock、scoped_lock 是专门用于管理锁资源的工具类。
1. std::lock_guard
std::lock_guard 是 C++11 引入的轻量级的 RAII 锁管理工具。它在构造时自动调用 lock() 加锁,在析构时自动调用 unlock() 解锁,可有效防止因异常或提前返回导致的锁泄漏。它不支持延迟加锁、超时加锁或锁所有权转移,适用于简单的锁应用场景。
#include <iostream>
#include <chrono>
#include <mutex>
#include <shared_mutex>
// 互斥锁
std::mutex mtx1;
// 递归锁
std::recursive_mutex mtx2;
// 读写锁
std::shared_mutex mtx3;
// 超时锁
std::timed_mutex mtx4;
void demo01()
{
// 构造函数中自动加锁,析构函数中自动解锁
{
std::lock_guard<std::mutex> lock(mtx1);
}
{
std::lock_guard<std::recursive_mutex> lock(mtx2);
}
// 注意:lock_guard 只能把读写锁当做互斥锁来使用,无法实现读写分离
{
std::lock_guard<std::shared_mutex> lock(mtx3);
}
// 注意:lock_guward 只能把超时锁当做普通锁来用,无法实现延迟加锁
{
std::lock_guard<std::timed_mutex> lock(mtx4);
}
// 简言之:std::lock_guard 不关心是什么锁,只会调用 lock 和 unlock 函数
}
void demo02()
{
mtx1.lock();
// 假设已经提前手动加锁
std::lock_guard<std::mutex> lock(mtx1, std::adopt_lock);
}
int main()
{
demo01();
return 0;
}
2. std::unique_lock
std::unique_lock 是 C++11 引入的锁管理工具,在构造时调用 lock() 加锁,在析构时调用 unlock() 解锁,它支持自动加锁、延迟加锁、尝试加锁、接管已有锁,适合更为精细的锁管理。并且支持移动语义,被认为是多线程编程中最通用、最灵活的锁管理方案。
#include <iostream>
#include <chrono>
#include <mutex>
#include <shared_mutex>
std::mutex mtx1;
std::shared_mutex mtx2;
std::recursive_mutex mtx3;
std::timed_mutex mtx4;
// 1. unique_lock 基本使用
void demo01()
{
// 1.1 管理互斥锁-标准用法
{
std::unique_lock<std::mutex> lock(mtx1);
std::cout << "持有 mutex" << std::endl;
// 自动解锁
}
// 1.2 手动管理-谨慎使用
{
std::unique_lock<std::mutex> lock(mtx1);
// 某些情况下需要提前解锁
lock.unlock();
// 这里可以执行非临界区操作
lock.lock(); // 重新加锁
// 自动解锁
}
// 1.3 释放管理权-高级用法
{
std::unique_lock<std::mutex> lock(mtx1);
std::mutex* p_mutex = lock.release();
// 现在需要手动管理
p_mutex->unlock();
}
// 1.4 管理读写锁(写)
{
std::unique_lock<std::shared_mutex> lock(mtx2);
std::cout << "持有 shared_mutex 写模式" << std::endl;
}
// 1.5 管理递归锁
{
std::unique_lock<std::recursive_mutex> lock(mtx3);
// 可以在同一线程递归加锁
std::unique_lock<std::recursive_mutex> lock2(mtx3);
std::cout << "持有递归锁" << std::endl;
}
}
// 2. unique_lock 对象构造
void demo02()
{
// 2.1 延迟加锁
{
// 构造时不加锁,需要手动 lock 加锁,加锁成功后,lock 负责锁的释放
std::unique_lock<std::mutex> lock(mtx1, std::defer_lock);
// 手动加锁
lock.lock();
// 手动解锁之后(也支持自动解锁),不会出现重复释放锁(安全)
lock.unlock();
}
// 2.2 接管锁
{
mtx1.lock();
std::unique_lock<std::mutex> lock(mtx1, std::adopt_lock);
}
// 2.3 尝试加锁
{
std::unique_lock<std::mutex> lock(mtx1, std::try_to_lock);
if (lock.owns_lock())
{
// 获取锁成功
}
else
{
// 获取锁失败
}
}
// 2.4 超时解锁
{
std::unique_lock<std::timed_mutex> lock(mtx4, std::chrono::seconds(3));
// std::unique_lock<std::timed_mutex> lock(mtx4, std::chrono::steady_clock::now() + std::chrono::seconds(3));
if (lock.owns_lock())
{
// 获取锁成功
}
else
{
// 获取锁失败
}
}
}
int main()
{
demo01();
demo02();
return 0;
}
3. std::shared_lock
std::shared_lock 是 C++17 引入的一种 RAII 锁管理工具,通常与 shared_mutex 或 shared_timed_mutex 配合使用。它用于自动管理共享锁(读锁),在构造时调用 lock_shared() 加锁,在析构时调用 unlock_shared() 解锁,它支持延迟加锁、尝试加锁、接管已有锁以及移动语义。
注意:std::shared_lock 仅能用于管理支持共享加锁接口的锁对象。
#include <iostream>
#include <shared_mutex>
#include <thread>
#include <vector>
std::shared_mutex mtx;
std::shared_timed_mutex tmtx;
int shared_data = 0;
void reader()
{
// 读操作需要使用 shared_lock
std::shared_lock<std::shared_mutex> lock(mtx);
std::cout << "读取数据: " << shared_data << std::endl;
}
void writer(int value)
{
// 写操作需要使用 unique_lock
std::unique_lock<std::shared_mutex> lock(mtx);
shared_data = value;
std::cout << " 写入数据: " << value << std::endl;
}
// 1. std::shared_lock 基本用法
void demo01()
{
std::vector<std::thread> readers;
std::vector<std::thread> writers;
// 先创建写线程
writers.emplace_back(writer, 100);
writers.emplace_back(writer, 200);
// 等待写线程完成
for (auto& t : writers) t.join();
// 然后创建读线程
for (int i = 0; i < 3; ++i)
readers.emplace_back(reader);
// 等待读线程完成
for (auto& t : readers) t.join();
}
// 2. std::shared_lock 对象构建
void demo02()
{
// 2.1 延迟解锁
{
std::shared_lock<std::shared_mutex> lock(mtx, std::defer_lock);
lock.lock();
}
// 2.2 接管锁
{
mtx.lock_shared();
std::shared_lock<std::shared_mutex> lock(mtx, std::adopt_lock);
}
// 2.3 尝试加锁
{
// 尝试进行读锁加锁,如果加锁成功,lock 负责锁的释放
std::shared_lock<std::shared_mutex> lock(mtx, std::try_to_lock);
if (lock.owns_lock())
{
std::cout << "成功获取读锁" << std::endl;
}
else
{
std::cout << "获取读锁失败" << std::endl;
}
}
// 2.4 超时加锁
{
// std::shared_lock<std::shared_timed_mutex> lock(tmtx, std::chrono::seconds(3));
std::shared_lock<std::shared_timed_mutex> lock(tmtx, std::chrono::steady_clock::now() + std::chrono::seconds(3));
if (lock.owns_lock())
{
// 获取锁成功
}
else
{
// 获取锁失败
}
}
}
int main()
{
demo01();
demo02();
return 0;
}
4. std::scoped_lock
在多线程程序中,我们常常需要同时操作多个锁。为了避免不同线程之间出现加锁顺序不一致导致的死锁问题,C++ 提供了 std::lock() 来一次性安全地锁定多个互斥量。
std::mutex mtx1, mtx2;
void demo()
{
std::lock(mtx1, mtx2);
// 进行相应操作
mtx1.unlock();
mtx2.unlock();
}
不过,这样的写法仍然存在风险:如果在加锁后的代码中提前返回或抛出异常,则可能导致锁未被正常释放,造成死锁或资源泄露。为了解决这一问题,C++17 引入了 std::scoped_lock,一种基于 RAII 的多锁管理工具,它能在作用域结束时自动解锁所有持有的锁,从而让多锁管理变得更安全、更简洁。
#include <iostream>
#include <mutex>
std::mutex mtx1, mtx2;
void demo()
{
// 用法1:自动锁定多个互斥量(推荐)
{
std::scoped_lock lock(mtx1, mtx2);
// 临界区 - 两个互斥量都已锁定
std::cout << "两个锁自动加锁" << std::endl;
} // 自动解锁
// 用法2:接管已锁定的互斥量(不推荐,容易出错)
{
std::lock(mtx1, mtx2);
// 注意:这里只传递互斥量,不重复锁定
std::scoped_lock<std::mutex, std::mutex> lock(std::adopt_lock, mtx1, mtx2);
std::cout << "接管已经加锁的锁" << std::endl;
} // 自动解锁
}
int main()
{
demo();
return 0;
}


冀公网安备13050302001966号