为什么需要 “锁” ?

  • 在多线程编程中,多个线程可能同时访问同一块内存(比如一个全局变量),这会导致 数据竞争(Data Race) ,从而产生不可预测的结果。举个例子:
    如下代码中,两个线程同时运行 increment(),最终 counter 的值很可能 不是 200000,因为 counter++ 实际上包含三步:
    1、读取 counter 的值;
    2、加 1;
    3、写回新值。
    如果两个线程交错执行这三步,就会互相覆盖结果,如下所示两次执行的结果不一样,结果也不是200000。
    PS:这里有个小疑问,如果 将 100000 换成一个比较小的数,例如 100,那么它的结果是确定的 200 ,这个疑问先暂存。
#include <iostream>
#include <thread>
using namespace std;

int counter = 0;

void increment() {
    for (int i = 0; i < 100000; ++i) {
        counter++; // 这行代码不是原子操作!
    }
}

int main() {
    thread t1(increment);
    thread t2(increment);

    t1.join();
    t2.join();
    cout << counter << endl;
}

在这里插入图片描述

  • C++11 标准(2011年发布)之前,C++ 没有标准的线程或同步机制,开发者只能依赖平台特定的 API(如 POSIX pthread、Windows API),导致代码不可移植。
    C++11 首次引入了标准的多线程支持,包括 <thread><mutex> 等。

  • 锁(Lock)的作用确保同一时间只有一个线程能执行某段关键代码(称为“临界区”),从而避免数据竞争

  • 同一时间只能有一个线程持有锁!!


一、锁

头文件:<mutex>

1.1 mutex 互斥锁

基本介绍

  • 作用
    最基本的锁,用于保护临界区,确保同一时间只有一个线程能进入。
  • 特性
    • 不可递归:⚠️ 同一线程重复调用 lock() 会导致未定义行为(通常死锁)。
    • ⚠️ mutex 对象不可拷贝、赋值、移动。
    • 非公平锁 :⚠️ 当多个线程在等待同一个 mutex 时,先等的线程不一定先拿到锁。

      这是因为:当一个线程释放 mutex 时,操作系统或底层实现可能直接把锁交给刚刚被唤醒的线程,或者正在运行的线程,而不是从等待队列头部取。
      这样做的好处是:减少上下文切换、提高吞吐量(性能更好)。坏处是:某些线程可能“饿死”(长时间等不到锁),虽然实际中很少见

  • 使用建议
    ⚠️ 永远不要直接使用 mutex::lock()/unlock() !应配合 RAII 封装类(如 lock_guard)使用,避免异常导致死锁。
  • 原型
class mutex {
public:
    constexpr mutex() noexcept;  // 默认构造
    ~mutex();                    // 析构(必须处于 unlocked 状态)

    mutex(const mutex&) = delete;  // 禁止拷贝
    mutex& operator=(const mutex&) = delete; // 禁止赋值

    void lock();                // 阻塞直到获得锁
    bool try_lock() noexcept;   // 尝试加锁,成功返回 true,否则返回 false
    void unlock();              // 解锁(必须由持有锁的线程调用)
};

▶ mutex 创建锁

  • 样例
#include <mutex>
using namespace std;

mutex  Mymutex1;

mutex Mymutex2 = Mymutex1; // 报错!⚠️ 禁止赋值
mutex Mymutex3(Mymutex1);  // 报错!⚠️ 禁止拷贝构造
mutex Mymutex4 = move(Mymutex1); // 报错!⚠️ 禁止移动

▶ lock() 加锁(阻塞)

void lock();
  • 样例
Mymutex1.lock();
  • 行为若当前未被锁定,则加锁并返回;否则 阻塞 当前线程,直到其他线程调用 unlock()。
  • 异常:若发生错误(如资源不足),可能抛出 std::system_error
  • 注意:⚠️ 不要在已经成功对某个 mutex 加锁的线程中,再次对该同一个 mutex 调用 lock(),会导致 未定义行为!!!。

    未定义行为 :程序 可能直接崩溃(比如死锁、段错误),可能陷入无限等待(因为线程在等自己释放锁),也可能看似“正常”运行,但这是偶然的,不可靠的,意味着 C++ 标准 不保证任何结果——编译器、操作系统、运行时环境可以做任何事!


▶ try_lock() 尝试加锁(非阻塞)

 bool try_lock() noexcept;
  • 样例
Mymutex1.try_lock();
  • 行为尝试立即加锁。若成功(即当前无人持有锁),返回 true 并加锁;否则立即返回 false,不阻塞。
  • 异常:声明为 noexcept,不会抛异常。
  • 用途:用于避免死锁、实现超时逻辑或非阻塞算法。

▶ unlock() 解锁

void unlock();
  • 样例
Mymutex1.unlock();
  • ⚠️ 使用前提是:当前线程必须是锁的持有者,且锁处于 locked 锁定 状态;否则会导致 未定义行为!!!
  • 行为释放锁,唤醒一个(或多个)等待线程。

实例

#include <mutex>
#include <iostream>
using namespace std;

mutex Mymutex;

int counter = 0;

void increment() {
    Mymutex.lock(); // 结果输出固定为 200000
    // Mymutex.try_lock(); // 结果输出 不定
    for (int i = 0; i < 100000; ++i) {
        counter++;
    }
    Mymutex.unlock();
}

int main() {
    thread t1(increment);
    thread t2(increment);

    t1.join();
    t2.join();
    cout << counter << endl;
}

1.2 recursive_mutex 递归锁

基本介绍

  • 背景
    为了 解决 同一线程在未释放锁的情况下再次尝试加锁的问题 ,比如上面 对同一线程内 已加锁 的 mutex 再次加锁会产生未定义的行为。常见于函数嵌套调用、公共接口调用内部工具函数等场景。

  • 作用
    允许同一线程多次成功加锁(lock)同一个互斥量,而不会导致死锁内部通过引用计数实现,每次 lock() 计数 +1,每次 unlock() 计数 -1;仅当计数归零时,锁才真正释放。 每次 lock() 必须对应一次 unlock(),只有当所有锁都被释放后,其他线程才能获取该锁。

  • 特性

    • ⚠️ recursive_mutex 对象禁止拷贝构造、移动、赋值操作。
    • ⚠️ 非公平锁
    • 性能略低于 mutex: 因为 需维护内部锁计数器和线程 ID,有轻微额外开销。
  • 使用建议
    和 mutex 一样,永远 不要直接调用 lock()/unlock()!应使用 lock_guard、unique_lock 等。

  • 原型

class recursive_mutex {
public:
    recursive_mutex();      // 默认构造
    ~recursive_mutex();     // 析构(必须处于 unlocked 状态)

    recursive_mutex(const recursive_mutex&) = delete;            // 禁止拷贝
    recursive_mutex& operator=(const recursive_mutex&) = delete; // 禁止赋值

    void lock();                // 阻塞加锁(可重入)
    bool try_lock() noexcept;   // 尝试加锁(可重入)
    void unlock();              // 解锁(必须由持有者调用)
};

▶ recursive_mutex 创建递归锁

  • 样例
#include <mutex>
using namespace std;

recursive_mutex rmtx1;

recursive_mutex rmtx2 = rmtx1;        // ❌ 报错!禁止拷贝
recursive_mutex rmtx3(rmtx1);         // ❌ 报错!禁止拷贝构造
recursive_mutex rmtx4 = move(rmtx1);  // ❌ 报错!禁止移动

▶ lock() 加锁(阻塞,可重入)

void lock();
  • 样例
rmtx1.lock(); // 第一次:成功
rmtx1.lock(); // 同一线程第二次:成功(计数=2)
  • 行为若当前线程已持有锁,则内部计数 +1 并立即返回;否则阻塞直到获得锁。
  • 异常:可能抛出 std::system_error
  • 注意:⚠️ 即使可重入,也必须 确保 unlock() 次数等于 lock() 次数 ,否则锁永不释放!

▶ try_lock() 尝试加锁(非阻塞,可重入)

bool try_lock() noexcept;
  • 样例
if (rmtx1.try_lock()) {
    // 成功加锁(可能是首次,也可能是重入)
    // ...
    rmtx1.unlock();
}
  • 行为若当前线程已持有锁,则计数 +1 并返回 true;否则尝试立即加锁,成功返回 true,失败返回 false。
  • 异常:声明为 noexcept不抛异常

▶ unlock() 解锁

void unlock();
  • 样例
rmtx1.unlock(); // 计数 -1
rmtx1.unlock(); // 再次 -1(若计数归零,则真正释放)
  • 前提: ⚠️unlock() 前,当前线程必须是锁的持有者,且锁处于 locked 状态(计数 > 0)。否则会产生未定义行为!!
  • 行为将内部锁计数减 1;若计数变为 0,则释放锁并唤醒等待线程。

实例

#include <iostream>
#include <mutex>
using namespace std;

int main() {
    recursive_mutex m;
    //mutex m; // 若换成 mutex,程序将死锁!
    
    cout << "Locking first time...\n";
    m.lock();
    cout << "Locked! Now trying to lock again...\n";
    
    m.lock();  // ← 同一线程再次加锁 → 死锁(在大多数 POSIX 系统上)
    cout << "This will NEVER print.\n";
    m.unlock();
    m.unlock();
}

在这里插入图片描述

1.3 自旋锁

  • 自旋锁(Spinlock) 是一种 忙等待 的同步机制:当线程尝试获取锁失败时,它不会阻塞或睡眠,而是线程保持运行态, 原地循环检查 即"自旋",直到锁被释放。
  • 适用场景
    自旋锁是 用 CPU 时间换上下文切换开销 的极端优化,仅在"等待时间远小于切换开销"时有效。绝大多数用户态代码应使用互斥锁,自旋锁主要存在于操作系统内核和极低延迟场景。
    1. 锁持有时间极短(如几条指令)
      上下文切换开销可能远大于临界区执行时间 ,自旋反而更快。
    2. 中断上下文 / 硬中断处理
      中断处理程序不能睡眠(阻塞),只能使用自旋锁。
    3. 多核实时系统
      避免调度延迟,保证确定性。
    4. 内核底层实现
      Linux 内核中大量使用自旋锁保护短临界区。
  • 致命缺点
    • CPU 空转浪费:自旋期间占用核心 100% 利用率。
    • 优先级反转:低优先级线程持有锁时,高优先级线程自旋可能饿死低优先级线程(无法释放锁)。
    • 单核噩梦:在单核 CPU 上,自旋锁持有者若被抢占,其他线程自旋将 永远等不到(需禁止抢占或中断)。

自旋锁 与 互斥锁 的本质区别

特性 自旋锁 互斥锁
等待机制 忙等待(循环检测) 阻塞等待(让出 CPU)
上下文切换 两次(阻塞+唤醒)
CPU 占用 高(持续自旋) 低(线程休眠)
适用场景 极短临界区、中断上下文 较长临界区、用户空间

二、锁管理器

2.1 lock_guard 自动 锁管理器

基本介绍

  • 作用
    RAII 风格的 互斥锁管理器

    💡 RAII :Resource Acquisition Is Initialization(资源获取即初始化),利用对象生命周期自动管理资源(如锁的释放)。

  • 局限性

    • ⚠️ 构造即加锁,不能延迟加锁
    • ⚠️不能手动解锁 :没有 unlock() 方法。
    • 若需更灵活控制,请用 unique_lock。
  • lock_guard 本身只是一个 锁管理器,多个锁管理器能否能对同一个 锁 再次加锁,完全取决于这个 锁类型是 否支持再次加锁。 例如:lock_guard<mutex> 就不支持对一个已加锁的 mutex 对象再次加锁。 lock_guard<recursive_mutex> 则支持再次加锁。

mutex m;
lock_guard<mutex> g1(m); // 第一次加锁 → OK
lock_guard<mutex> g2(m); // 同一线程再次加锁同一个 mutex → UB!
  • 原型
template <class Mutex>
class lock_guard {
public:
    using mutex_type = Mutex;

    explicit lock_guard(Mutex& m);           // 加锁
    lock_guard(Mutex& m, std::adopt_lock_t); // 假设 m 已被当前线程锁定(不加锁)
    ~lock_guard();                           // 自动解锁

    lock_guard(const lock_guard&) = delete;
    lock_guard& operator=(const lock_guard&) = delete;
};

▶ 自动 加/解锁

  • 作用构造时自动加锁,析构时自动解锁。

  • 样例

#include <mutex>
using namespace std;

mutex  mtx;
lock_guard<mutex> Mylockguard1(mtx); // 自动加锁


lock_guard<mutex> Mylockguard2 = Mylockguard1; // 报错!⚠️ 禁止赋值
mutex Mylockguard3(Mylockguard1);  // 报错!⚠️ 禁止拷贝构造
mutex Mylockguard4 = move(Mylockguard1); // 报错!⚠️ 禁止移动

▶ 自动 解锁

  • 前提:⚠️所管理的 锁 已提前被加锁。否则会引发未定义行为!

  • 作用构造时 自动加锁,仅管理已加锁的锁,析构时自动解锁。

  • 样例

mutex mtx;

lock_guard<mutex> Myadoptlock0(mtx,adopt_lock); // 错误!⚠️ 所管理的锁必须得提前加锁!!!

mtx.lock();
lock_guard<mutex> Myadoptlock(mtx,adopt_lock); // 自动解锁

实例

实例1:自动加锁/解锁
#include <mutex>
#include <iostream>
using namespace std;

mutex mtx;

int counter = 0;

void increment() {
    lock_guard<mutex> Mylockguard(mtx);
    for (int i = 0; i < 100000; ++i) {
        counter++;
    }
}

int main() {
    thread t1(increment);
    thread t2(increment);

    t1.join();
    t2.join();
    cout << counter << endl; // 输出 200000
}

实例2:自动解锁
  • 死锁问题:
#include <mutex>
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;

mutex mtxA, mtxB;

void lock_A_then_B() {
    cout << "Thread 1: trying to lock A...\n";
    lock_guard<mutex> g1(mtxA);
    cout << "Thread 1: locked A, now trying to lock B...\n";
    
    // 模拟一些处理时间,增加与另一线程交叉的机会
    this_thread::sleep_for(chrono::milliseconds(100));
    
    lock_guard<mutex> g2(mtxB); // 此处可能永远阻塞!
    cout << "Thread 1: locked B, doing work...\n";
}

void lock_B_then_A() {
    cout << "Thread 2: trying to lock B...\n";
    lock_guard<mutex> g1(mtxB);
    cout << "Thread 2: locked B, now trying to lock A...\n";
    
    // 模拟处理时间
    this_thread::sleep_for(chrono::milliseconds(100));
    
    lock_guard<mutex> g2(mtxA); // 此处可能永远阻塞!
    cout << "Thread 2: locked A, doing work...\n";
}

int main() {
    thread t1(lock_A_then_B);
    thread t2(lock_B_then_A);

    t1.join(); // ← 程序会卡在这里(死锁)
    cout << "here" << endl; // 这行不会执行
    t2.join();

    cout << "Program finished.\n"; // 这行也不会执行!
    return 0;
}

在这里插入图片描述

  • 解决方法:

std::lock(mtxA, mtxB) 的作用
这是一个原子操作:它会以某种内部策略(如尝试-回退)确保两个锁被同时获取。
无论线程以什么顺序调用,都不会出现“各持一个、互相等待”的情况。
它保证:要么两个都锁成功,要么不断重试直到成功(不会部分持有)。

#include <mutex>
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;

mutex mtxA, mtxB;

// 安全版本:避免死锁
void safe_operation(const string& name) {
    cout << name << ": preparing to lock both mutexes...\n";
    
    // Step 1: 使用 lock 同时锁定两个 mutex(无死锁)
    lock(mtxA, mtxB);
    
    // Step 2: 用 adopt_lock 告诉 lock_guard:“锁已经加好了,请只负责析构时 unlock”
    lock_guard<mutex> g1(mtxA, adopt_lock);
    lock_guard<mutex> g2(mtxB, adopt_lock);

    cout << name << ": locked both A and B, doing work...\n";
    
    // 模拟工作(现在可以安全休眠,不会导致死锁)
    this_thread::sleep_for(chrono::milliseconds(100));
    
    cout << name << ": finished work.\n";
    // g1 和 g2 析构时自动 unlock(顺序无关)
}

int main() {
    thread t1([]() { safe_operation("Thread 1"); });
    thread t2([]() { safe_operation("Thread 2"); });

    t1.join();
    t2.join();

    cout << "Program finished successfully! No deadlock.\n";
    return 0;
}

在这里插入图片描述


2.2 unique_lock 灵活锁 管理器

基本介绍

  • 作用
    比 lock_guard 更灵活的锁管理器,支持延迟加锁、手动解锁、转移所有权等

  • 原型

template <class Mutex>
class unique_lock {
public:
    using mutex_type = Mutex;

    // (1) ✅️默认构造(无关联 mutex)
    unique_lock() noexcept;

    // (2) ✅️关联 mutex,但不加锁
    explicit unique_lock(Mutex& m, std::defer_lock_t) noexcept;

    // (3) ✅️关联并加锁
    explicit unique_lock(Mutex& m);

    // (4) 关联并尝试加锁(非阻塞)
    unique_lock(Mutex& m, std::try_to_lock_t);

    // (5) 接管已加锁的 mutex
    unique_lock(Mutex& m, std::adopt_lock_t);

    // (6) 支持超时(配合 timed_mutex)
    unique_lock(Mutex& m, const std::chrono::duration<Rep, Period>& timeout_duration);
    unique_lock(Mutex& m, const std::chrono::time_point<Clock, Duration>& timeout_time);

    ~unique_lock(); // ✅️若仍持有锁,则 unlock()

    // 核心操作
    void lock();
    bool try_lock();
    template<class Rep, class Period>
    bool try_lock_for(const std::chrono::duration<Rep, Period>& rel_time);
    template<class Clock, class Duration>
    bool try_lock_until(const std::chrono::time_point<Clock, Duration>& abs_time);
    void unlock();

    // 状态查询
    bool owns_lock() const noexcept;
    explicit operator bool() const noexcept { return owns_lock(); }
    Mutex* mutex() const noexcept;

    // 所有权转移(move-only)
    unique_lock(unique_lock&& other) noexcept;  
    unique_lock& operator=(unique_lock&& other) noexcept;
};

▶ unique_lock 的创建

  • ⚠️ 支持移动语义,不支持拷贝、赋值!
  • 样例
mutex mtx;
unique_lock<mutex> lock1(mtx);
unique_lock<mutex> lock2 = move(lock1); // 移动赋值,现在 lock2 拥有锁,lock1 不再持有


unique_lock<mutex> a(mtx);
unique_lock<mutex> b(lock1);    // ❌ 错误!禁止拷贝
unique_lock<mutex> c = lock1;   // ❌ 错误!禁止赋值

▶ defer_lock 延迟加锁

unique_lock<mutex> ulock(Mymutelock, defer_lock);

// ... 做些准备 ...

ulock.lock(); // 手动加锁

▶ 配合条件变量(必须用 unique_lock)

condition_variable cv;
mutex mtx;
bool ready = false;

// 等待线程
unique_lock<std::mutex> ulock(mtx);
cv.wait(lock, []{ return ready; }); // wait 会临时 unlock,唤醒后 relock

实例

#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <chrono>

std::mutex log_mutex;
std::vector<std::string> shared_log;

void log_message(const std::string& msg) {
    // Step 1: 加锁,安全写入内存队列
    std::unique_lock<std::mutex> lock(log_mutex);
    shared_log.push_back(msg);
    std::cout << "Logged: " << msg << " (size=" << shared_log.size() << ")\n";
    
    // Step 2: 暂时释放锁!因为接下来要模拟“写磁盘”(耗时 I/O)
    lock.unlock(); // ✅ 关键:释放锁,让其他线程可以继续写日志
    
    // 模拟耗时 I/O 操作(比如写文件、发网络请求)
    std::this_thread::sleep_for(std::chrono::milliseconds(500));
    
    // Step 3: 如果需要再次访问 shared_log,可以重新加锁
    lock.lock(); // ✅ 重新加锁
    if (!shared_log.empty()) {
        // 假装做点检查
        std::cout << "Still " << shared_log.size() << " messages in buffer.\n";
    }
    // 析构时自动 unlock(如果还持有)
}

int main() {
    std::thread t1([] { log_message("Error: Disk full"); });
    std::thread t2([] { log_message("Warning: High CPU"); });
    std::thread t3([] { log_message("Info: User logged in"); });

    t1.join();
    t2.join();
    t3.join();

    std::cout << "All logs processed. Final buffer size: " << shared_log.size() << "\n";
    return 0;
}

在这里插入图片描述


三、辅助函数:std::lock()

基本介绍

  • 作用
    同时锁定多个 mutex,避免死锁 (使用某种死锁避免算法,如“先尝试全部 lock,失败则回退重试”)。
  • 原型
template <class Lockable1, class Lockable2, class... LockableN>
void lock(Lockable1& m1, Lockable2& m2, LockableN&... mN);

  • 该函数本身不提供 RAII,通常配合 adopt_lock + lock_guard/unique_lock 使用。
  • C++17 后推荐直接用 scoped_lock

实例

详见上面 lock_guard 的 实例2。


四、小结

  • 会引发未定义行为的动作:
    1. 当前线程的 std::mutex 已 locked 加锁, 却又再次 lock() 加锁。
    2. unlock() 前,当前线程必须是锁的持有者,且锁处于 locked 状态,否则会产生未定义行为。
    3. 使用 lock_guard Myadoptlock(mtx,adopt_lock); 构造自动解锁管理器时,所管理的锁未提前加锁,
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐