在多线程编程中,多个线程同时访问同一资源可能会引发竞态条件(Race Condition),导致数据不一致或程序崩溃。为了解决这个问题,需要一种机制来确保同一时间只有一个线程能够访问共享资源,这就是互斥锁的作用。C++ 标准库提供了最基础也是最常用的互斥锁类型 std::mutex。
1. 问题场景
#include <iostream>
#include <thread>
int counter = 0;
void increment()
{
for (int i = 0; i < 100000; ++i)
{
counter++;
}
}
int main()
{
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "最终结果: " << counter << "(期望: 200000)" << std::endl;
return 0;
}
2. 锁的用法
在深入多线程编程之前,我们先了解 std::mutex 的基本操作方法。
#include <iostream>
#include <thread>
#include <mutex>
int counter = 0;
// 初始化锁
std::mutex mtx;
void increment()
{
for (int i = 0; i < 100000; ++i)
{
mtx.lock();
counter++;
mtx.unlock();
}
}
void demo01()
{
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "最终结果: " << counter << "(期望: 200000)" << std::endl;
}
void demo02()
{
mtx.lock();
bool flag = mtx.try_lock();
if (flag)
{
mtx.unlock();
}
else
{
std::cout << "做点什么事情" << std::endl;
}
mtx.unlock();
}
int main()
{
demo01();
demo02();
return 0;
}
- 不可拷贝/移动:
std::mutex不能被拷贝或移动,保证每把锁唯一
- 加锁解锁要求:
- 每次
lock()必须对应一次unlock(),成对出现 - 多次
lock()或多次unlock()的行为是未定义的 unlock()必须由持有锁的线程执行
- 每次
- 异常与提前返回:
- 在
unlock()之前发生异常或提前返回,会导致锁无法释放 - 推荐使用 RAII(如
std::lock_guard或std::scoped_lock)自动管理锁
- 在
- 死锁风险:
- 多个线程同时锁多个 mutex 时,如果加锁顺序不一致,可能发生死锁
- 建议多个锁按固定顺序加锁,或者使用
std::lock一次性加锁多个 mutex
- 临界区大小影响:
- 临界区小:持锁时间短,线程等待少,并发效率高,但频繁加解锁可能带来额外开销
- 临界区大:持锁时间长,线程等待多,并发效率下降
- 最佳实践:临界区只保护访问共享资源的必要代码,耗时计算、I/O 或非共享操作尽量放在临界区外
2. 锁的源码
class _Mutex_base { // base class for all mutex types
public:
// 初始化底层锁和状态
constexpr _Mutex_base(int _Flags = 0) noexcept {
// 操作系统提供的低级锁对象
_Mtx_storage._Critical_section = {};
// 初始化线程 ID,-1 表示无线程持有锁
_Mtx_storage._Thread_id = -1;
// 设置锁类型标志,默认支持 try_lock
_Mtx_storage._Type = _Flags | _Mtx_try;
// 当前线程加锁次数(递归锁用)
_Mtx_storage._Count = 0;
}
_Mutex_base(const _Mutex_base&) = delete;
_Mutex_base& operator=(const _Mutex_base&) = delete;
void lock() {
// _Mymtx() 获取底层锁对象
// _Mtx_lock() 阻塞式枷锁,会等待直到锁可用
// 返回 _Success 表示加锁成功
// 如果返回其他值(如系统错误),抛出异常
if (_Mtx_lock(_Mymtx()) != _Thrd_result::_Success) {
_STD _Throw_Cpp_error(_RESOURCE_DEADLOCK_WOULD_OCCUR);
}
// 该函数用于验证递归加锁的计数(_Count)是否即将溢出(_Count == INT_MAX)
if (!_Verify_ownership_levels()) {
_STD _Throw_Cpp_error(_RESOURCE_UNAVAILABLE_TRY_AGAIN);
}
}
bool try_lock() noexcept {
// 非阻塞加锁,尝试加锁,成功返回 true,失败返回 false
return _Mtx_trylock(_Mymtx()) == _Thrd_result::_Success;
}
void unlock() noexcept {
// 释放锁
_Mtx_unlock(_Mymtx());
}
protected:
bool _Verify_ownership_levels() noexcept {
if (_Mtx_storage._Count == INT_MAX) {
--_Mtx_storage._Count;
return false;
}
return true;
}
private:
// 声明友元类,在这些类中可以访问当前类的私有成员
friend condition_variable;
friend condition_variable_any;
// 底层互斥锁对象
_Mtx_internal_imp_t _Mtx_storage{};
_Mtx_t _Mymtx() noexcept {
return &_Mtx_storage;
}
};
class mutex : public _Mutex_base {
public:
mutex() noexcept = default;
// 禁止拷贝和移动
mutex(const mutex&) = delete;
mutex& operator=(const mutex&) = delete;
};
4. 锁的开销
当多个线程竞争同一把锁时,未获取到锁的线程无法继续执行,此时操作系统会将其从运行状态挂起,并调度其他就绪线程执行,这个过程涉及线程上下文切换。而当锁被释放、挂起线程重新获得锁时,又需要从挂起状态恢复为运行状态,再次触发上下文切换。
上下文切换的开销不容忽视,它需要保存和恢复当前线程的运行状态,整个过程会占用大量 CPU 周期,尤其在高频竞争场景下,频繁的切换会严重消耗系统资源。
为缓解上下文切换的高昂代价,大多数现代 C++ 标准库中的 std::mutex 实现采用自适应自旋策略:当线程首次尝试获取锁失败时,不会立即挂起,而是先进入一段自旋等待,CPU 持续空转,反复检查锁的状态,若锁持有时间较短(比如临界区操作简单),自旋等待的短暂 CPU 空转开销,远小于线程挂起、恢复的上下文切换开销。但如果锁竞争激烈、持有时间过长,自旋等待会持续浪费 CPU 资源,此时std::mutex 会自动切换策略,将线程挂起。
锁的开销与竞争激烈程度直接正相关:竞争越激烈,自旋等待的无效消耗、上下文切换的代价都会显著上升。因此,优化锁开销的核心思路是尽可能减少锁竞争,常见手段就是缩小临界区范围:仅将真正需要原子性保护的操作放入临界区内,让线程持有锁的时间最短化。

冀公网安备13050302001966号