C++ std::mutex 互斥锁

在多线程编程中,多个线程同时访问同一资源可能会引发竞态条件(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_guardstd::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 会自动切换策略,将线程挂起。​

锁的开销与竞争激烈程度直接正相关:竞争越激烈,自旋等待的无效消耗、上下文切换的代价都会显著上升。因此,优化锁开销的核心思路是尽可能减少锁竞争,常见手段就是缩小临界区范围:仅将真正需要原子性保护的操作放入临界区内,让线程持有锁的时间最短化。

    未经允许不得转载:一亩三分地 » C++ std::mutex 互斥锁
    评论 (0)

    4 + 4 =