一、基础概念:锁的核心分类
在讲解具体工具前,先明确c++锁的两个核心维度:
- 基础锁类型(提供原始的加锁/解锁能力):std::mutex、std::recursive_mutex、std::timed_mutex、std::recursive_timed_mutex;
- raii锁封装(管理基础锁的生命周期,避免手动解锁漏解/死锁):std::lock_guard、std::unique_lock、std::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_variable的wait()方法必须接收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_lock的adopt_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 | 单次调用,无需手动加锁判断 | 仅单次执行 | 初始化、单例创建 |
选型口诀
- c++17及以上:优先用scoped_lock(单锁/多锁),复杂逻辑用unique_lock;
- c++11/14:单锁用lock_guard,多锁用std::lock + lock_guard,复杂逻辑用unique_lock;
- 读多写少:用std::shared_mutex + shared_lock/unique_lock;
- 单次初始化:用std::call_once;
- 条件变量:必须用unique_lock。
六、总结
- 基础锁:std::mutex是核心,递归场景用recursive_mutex,超时场景用timed_mutex;
- raii封装:
- 简单场景:lock_guard(c++11/14)/scoped_lock(c++17+);
- 复杂场景:unique_lock(灵活、支持条件变量/超时);
- 特殊场景:
- 读多写少:shared_mutex + shared_lock;
- 单次执行:call_once;
- 核心原则:永远用raii封装管理锁,避免手动lock()/unlock(),防止死锁。
到此这篇关于c++线程锁的使用的文章就介绍到这了,更多相关c++线程锁内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论