一、基础概念:锁的核心分类

在讲解具体工具前,先明确C++锁的两个核心维度:

  1. 基础锁类型(提供原始的加锁/解锁能力):std::mutexstd::recursive_mutexstd::timed_mutexstd::recursive_timed_mutex
  2. RAII锁封装(管理基础锁的生命周期,避免手动解锁漏解/死锁):std::lock_guardstd::unique_lockstd::scoped_lock(C++17)。

二、基础锁类型(原始锁)

这类锁是“底层同步工具”,提供lock()unlock()try_lock()等核心方法,但不建议直接使用(手动解锁易出错),需配合RAII封装使用。

锁类型 核心特性 适用场景 注意事项
std::mutex 最基础的互斥锁,非递归、非超时 绝大多数单资源互斥场景 同一线程重复lock()会触发未定义行为(崩溃)
std::recursive_mutex 递归互斥锁,同一线程可多次lock()(需对应次数unlock() 函数嵌套调用需要加锁的场景 性能略低于std::mutex,避免滥用
std::timed_mutex 带超时的互斥锁,支持try_lock_for()(超时时间)、try_lock_until()(截止时间) 需要“非阻塞等待锁”的场景 超时返回false,避免永久阻塞
std::recursive_timed_mutex 递归+超时的互斥锁,结合前两者特性 递归调用+超时等待的场景 尽量少用,递归锁易隐藏逻辑问题
基础锁使用示例(不推荐直接用,仅演示)
#include <mutex>
#include <iostream>
std::mutex m;

void bad_use() {
    m.lock(); // 手动加锁
    std::cout << "临界区操作" << std::endl;
    // 若此处抛异常,unlock()不会执行 → 死锁!
    m.unlock(); // 手动解锁
}

核心问题:手动管理lock()/unlock()易因异常、逻辑分支遗漏解锁,导致死锁——这也是RAII封装的核心价值。

三、RAII锁封装(推荐使用)

RAII(资源获取即初始化)的核心思想:构造时获取资源,析构时释放资源。锁封装会在构造函数中调用lock()(或接管已锁定的锁),析构函数中调用unlock(),无论函数正常退出还是异常退出,锁都会被释放。

1. std::lock_guard(极简RAII锁,C++11)
核心特性
  • 不可移动、不可复制,生命周期与作用域绑定;
  • 构造时必须指定锁,且默认立即加锁
  • 析构时自动解锁,无其他额外功能(极简、高效);
  • 不支持手动解锁、不支持超时、不支持延迟加锁。
使用示例(最常用场景)
#include <mutex>
#include <list>
std::list<int> data;
std::mutex m;

void safe_add(int val) {
    // 构造时加锁,函数结束析构时解锁
    std::lock_guard<std::mutex> guard(m);
    data.push_back(val); // 临界区操作,线程安全
}
进阶用法:接管已锁定的锁(std::adopt_lock)

当锁已被std::lock()手动锁定时,用std::adopt_lock标记告诉lock_guard“锁已锁定,只需接管解锁责任”:

void swap_data(std::list<int>& a, std::list<int>& b, std::mutex& m1, std::mutex& m2) {
    if (&a == &b) return;
    std::lock(m1, m2); // 同时锁定两个锁,避免死锁
    // adopt_lock:不重复加锁,仅接管解锁
    std::lock_guard<std::mutex> g1(m1, std::adopt_lock);
    std::lock_guard<std::mutex> g2(m2, std::adopt_lock);
    a.swap(b); // 临界区操作
}
适用场景
  • 简单的“作用域内加锁”场景,无需手动控制锁的生命周期;
  • 追求极致性能(无额外开销),只需“加锁→操作→解锁”的固定流程。
2. std::unique_lock(灵活RAII锁,C++11)
核心特性
  • 不可复制、可移动,生命周期可手动控制;
  • 支持延迟加锁(构造时不加锁,后续手动lock());
  • 支持手动解锁(unlock())、重新加锁(lock());
  • 支持超时加锁(try_lock_for()try_lock_until(),需配合timed_mutex);
  • 支持std::adopt_lock接管已锁定的锁;
  • 性能略高于lock_guard(但可忽略),功能远更灵活。
核心用法示例
(1)延迟加锁(std::defer_lock)
#include <mutex>
std::timed_mutex tm;

void flexible_lock() {
    // defer_lock:构造时不加锁,仅关联锁对象
    std::unique_lock<std::timed_mutex> ul(tm, std::defer_lock);
    
    // 手动加锁(也可尝试超时加锁)
    if (ul.try_lock_for(std::chrono::seconds(1))) {
        std::cout << "获取锁成功,执行临界区操作" << std::endl;
        ul.unlock(); // 手动解锁(可提前释放锁,提高并发)
        // ... 其他非临界区操作
        ul.lock(); // 重新加锁
    } else {
        std::cout << "1秒内未获取锁,执行降级逻辑" << std::endl;
    }
    // 析构时:如果锁仍被持有,自动解锁
}
(2)配合条件变量(唯一适用场景)

std::condition_variablewait()方法必须接收std::unique_lock(因为wait()会临时解锁,被唤醒后重新加锁,lock_guard无此能力):

#include <mutex>
#include <condition_variable>
#include <queue>

std::queue<int> q;
std::mutex m;
std::condition_variable cv;

void producer(int val) {
    std::lock_guard<std::mutex> lg(m);
    q.push(val);
    cv.notify_one(); // 通知消费者
}

void consumer() {
    std::unique_lock<std::mutex> ul(m);
    // wait()会解锁,等待通知;被唤醒后重新加锁,再判断条件
    cv.wait(ul, [](){ return !q.empty(); });
    int val = q.front();
    q.pop();
    ul.unlock(); // 提前解锁,提高并发
    std::cout << "消费:" << val << std::endl;
}
适用场景
  • 需要手动控制锁的生命周期(提前解锁、重新加锁);
  • 需要超时等待锁的场景;
  • 配合std::condition_variable使用(唯一选择);
  • 复杂的加锁逻辑(如条件加锁、动态解锁)。
3. std::scoped_lock(C++17,lock_guard的升级版)
核心特性
  • 不可移动、不可复制,作用域绑定;
  • 支持同时锁定多个锁(内置std::lock()的死锁避免逻辑);
  • 性能与lock_guard一致,语法更简洁;
  • 可视为“多锁版本的lock_guard”。
使用示例(替代lock_guard+std::lock)
#include <mutex>
#include <list>

std::list<int> a, b;
std::mutex m1, m2;

void safe_swap() {
    // 同时锁定m1和m2,自动避免死锁,析构时同时解锁
    std::scoped_lock guard(m1, m2);
    a.swap(b); // 临界区操作
}

对比旧写法(lock_guard+std::lock):

std::lock(m1, m2);
std::lock_guard<std::mutex> g1(m1, std::adopt_lock);
std::lock_guard<std::mutex> g2(m2, std::adopt_lock);

scoped_lock一行搞定,更简洁、不易出错。

适用场景
  • 需要同时锁定多个锁的场景(替代std::lock + lock_guard);
  • C++17及以上环境,优先选择scoped_lock而非lock_guard(功能更强,无性能损失)。

四、其他锁相关工具

1. std::call_once(单次调用锁)
  • 核心作用:保证某个函数在多线程环境下仅执行一次(比如单例的初始化);
  • 底层依赖std::once_flag,比手动加锁判断更高效、更安全。
使用示例
#include <mutex>
#include <iostream>

std::once_flag flag;
void init() {
    std::cout << "仅执行一次的初始化逻辑" << std::endl;
}

void thread_func() {
    std::call_once(flag, init); // 多线程调用,init仅执行一次
}
2. std::shared_mutex / std::shared_lock(C++17,读写锁)
  • 核心思想:区分“读操作”和“写操作”,允许多个读线程同时持有锁,写线程独占锁(提高读多写少场景的并发);
    • std::shared_lock:共享锁(读锁),多个线程可同时持有;
    • std::unique_lock:独占锁(写锁),仅一个线程持有。
使用示例
#include <shared_mutex>
#include <string>
#include <iostream>

std::string data = "初始数据";
std::shared_mutex sm;

// 读线程:共享锁
void read_data(int id) {
    std::shared_lock<std::shared_mutex> sl(sm);
    std::cout << "读线程" << id << ":" << data << std::endl;
}

// 写线程:独占锁
void write_data(const std::string& new_data) {
    std::unique_lock<std::shared_mutex> ul(sm);
    data = new_data;
    std::cout << "写线程:更新数据为" << new_data << std::endl;
}

优势:读多写少场景下,并发效率远高于普通互斥锁(普通锁无论读写都独占)。

3. std::lock(通用锁函数)
  • 核心作用:原子地锁定多个锁,避免死锁(内部实现按地址排序锁定);
  • 配合lock_guard/unique_lockadopt_lock使用(C++17前);
  • C++17后优先用scoped_lock,无需手动调用std::lock

五、核心对比与选型建议

工具 核心优势 核心限制 优先选型场景
std::lock_guard 极简、高效 不可手动解锁、不支持多锁 C++11/14,单锁、简单作用域加锁
std::unique_lock 灵活(延迟/超时/手动解锁) 略高开销(可忽略) 条件变量、超时等待、动态解锁
std::scoped_lock 多锁、简洁、高效 C++17+ C++17+,单锁/多锁、所有简单场景
std::shared_mutex 读写分离,读多写少并发高 C++17+ 读多写少的共享资源访问
std::call_once 单次调用,无需手动加锁判断 仅单次执行 初始化、单例创建
选型口诀
  1. C++17及以上:优先用scoped_lock(单锁/多锁),复杂逻辑用unique_lock
  2. C++11/14:单锁用lock_guard,多锁用std::lock + lock_guard,复杂逻辑用unique_lock
  3. 读多写少:用std::shared_mutex + shared_lock/unique_lock
  4. 单次初始化:用std::call_once
  5. 条件变量:必须用unique_lock

六、总结

  1. 基础锁std::mutex是核心,递归场景用recursive_mutex,超时场景用timed_mutex
  2. RAII封装
    • 简单场景:lock_guard(C++11/14)/scoped_lock(C++17+);
    • 复杂场景:unique_lock(灵活、支持条件变量/超时);
  3. 特殊场景
    • 读多写少:shared_mutex + shared_lock
    • 单次执行:call_once
  4. 核心原则:永远用RAII封装管理锁,避免手动lock()/unlock(),防止死锁。
Logo

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

更多推荐