关于c++中锁的种类与使用
Optimized tool selectionC++ 标准库中有多种锁管理机制,各有不同的特性和适用场景:
看这段代码中使用的是 std::lock_guard,这是一个自动管理锁的工具。锁会在以下时刻释放:
锁的生命周期
cpp
.....
static std::mutex g_mutex; // 保护 g_control / g_reply 的并发访问
.....
void handleControlGram(const ControlGram &cg) {
std::lock_guard<std::mutex> lk(g_mutex); // ← 获取锁
g_control.SteeringThrottle = (uint8_t)cg.SteeringThrottle;
// ... 其他赋值操作
g_control.ControlMode = cg.ControlMode;
} // ← 函数结束时,lk 对象析构,锁自动释放
锁的释放时机:
- 函数返回时 ---
lock_guard对象超出作用域,析构函数被调用,锁自动释放 - 无需手动调用
unlock()
为什么使用 lock_guard?
这是 RAII 模式(Resource Acquisition Is Initialization)的应用:
| 情形 | 手动 lock/unlock | lock_guard |
|---|---|---|
| 正常执行 | ✓ 释放 | ✓ 自动释放 |
| 异常抛出 | ✗ 锁泄露 | ✓ 析构仍会释放 |
| 逻辑分支 | ✗ 容易遗漏 | ✓ 自动保证 |
在 C++ 中,锁 是解决多线程数据竞争 的核心工具(C++11 及以后标准提供了完善的锁机制)。针对你的 NVIDIA Orin(Ubuntu20.04,ARM 架构),所有标准 C++ 锁都可以直接使用,编译时仅需链接线程库(-pthread)。
我会分 底层互斥体(锁本体) 、RAII 自动锁管理工具(推荐必用) 两部分讲解,包含种类、特性、区别、使用场景、代码示例,最后给避坑指南。
一、前置核心知识
-
作用 :保护共享资源(变量、文件、硬件接口),保证同一时间只有一个/指定线程访问,避免数据错乱。
-
核心头文件 :
cpp#include <mutex> // C++11 基础锁 #include <shared_mutex> // C++17 读写锁 #include <thread> // 线程 -
黄金原则 :永远用 RAII 自动管理锁 ,不要手动
lock()/unlock()(极易忘记解锁导致死锁)。 -
编译命令 (Orin 通用):
bashg++ -std=c++17 代码.cpp -o 程序 -pthread
二、第一部分:C++ 标准底层互斥体(锁的本体)
这是锁的核心实现,分为 独占锁、递归锁、超时锁、读写锁 4 大类,共 5 种:
| 互斥体类型 | 独占性 | 递归 | 超时 | 共享读写 | C++ 版本 | 核心特性 |
|---|---|---|---|---|---|---|
std::mutex |
✅ 独占 | ❌ | ❌ | ❌ | C++11 | 最基础、性能最高 |
std::recursive_mutex |
✅ 独占 | ✅ | ❌ | ❌ | C++11 | 同一线程可重复加锁 |
std::timed_mutex |
✅ 独占 | ❌ | ✅ | ❌ | C++11 | 可尝试加锁,超时放弃 |
std::recursive_timed_mutex |
✅ 独占 | ✅ | ✅ | ❌ | C++11 | 递归+超时 |
std::shared_mutex |
✅ 独占 | ❌ | ❌ | ✅ 读写分离 | C++17 | 读共享、写独占 |
1. std::mutex(基础独占锁)
- 特性 :独占排他锁 ,同一时间只能被一个线程持有;不可递归、不可超时、不可共享。
- 适用场景:90% 的常规多线程场景(简单共享变量保护)。
- 禁止操作:同一个线程不能加锁两次(直接死锁)。
2. std::recursive_mutex(递归独占锁)
- 特性 :允许同一个线程多次加锁 ,必须解锁相同次数才会释放;其他特性同
mutex。 - 适用场景 :递归函数、嵌套调用中需要加锁的场景(极少用,能不用就不用)。
3. std::timed_mutex(超时独占锁)
- 特性 :支持超时尝试加锁,不会永久阻塞线程。
- API :
try_lock_for(时间)、try_lock_until(时间点)。 - 适用场景:需要避免永久死等锁的场景(如硬件通信、超时任务)。
4. std::shared_mutex(读写锁 / 共享互斥锁,C++17)
- 核心特性 :读写分离
- 读模式(共享):多个线程可以同时加锁读数据;
- 写模式(独占):只有一个线程能加锁写数据,写时禁止读。
- 适用场景 :读多写少(如配置读取、日志、缓存),性能远高于普通独占锁。
三、第二部分:RAII 自动锁管理工具(必用!)
直接操作底层互斥体容易忘记解锁,C++ 提供了 4 种自动锁工具,构造时加锁,析构时自动解锁:
| 管理工具 | 配合锁类型 | 手动解锁 | 超时支持 | 多锁同时加锁 | 核心用途 |
|---|---|---|---|---|---|
std::lock_guard |
所有独占锁 | ❌ | ❌ | ❌ | 最简单、最常用(默认首选) |
std::unique_lock |
所有独占锁 | ✅ | ✅ | ❌ | 灵活锁(配合条件变量、手动控制) |
std::shared_lock |
shared_mutex | ✅ | ✅ | ❌ | 读写锁的读模式(共享) |
std::scoped_lock |
所有锁 | ❌ | ❌ | ✅ | C++17 同时锁多个锁,防死锁 |
四、完整使用示例(可直接在 Orin 上运行)
示例 1:基础用法 std::mutex + lock_guard(首选)
最常用,保护简单共享变量:
cpp
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;
mutex mtx; // 全局互斥锁
int share_num = 0; // 共享资源
void add_num() {
// 构造自动加锁,离开作用域自动解锁
lock_guard<mutex> lock(mtx);
share_num++;
cout << "线程ID: " << this_thread::get_id() << " 数值: " << share_num << endl;
}
int main() {
thread t1(add_num);
thread t2(add_num);
t1.join();
t2.join();
return 0;
}
示例 2:递归锁 recursive_mutex
解决递归函数加锁问题:
cpp
#include <mutex>
recursive_mutex rmtx;
void func(int n) {
lock_guard<recursive_mutex> lock(rmtx);
if (n > 0) func(n-1); // 同一线程重复加锁,无死锁
}
示例 3:超时锁 timed_mutex + unique_lock
避免永久阻塞:
cpp
#include <mutex>
#include <chrono>
timed_mutex tmtx;
void try_lock_func() {
// 尝试加锁,最多等待 100ms
unique_lock<timed_mutex> lock(tmtx, chrono::milliseconds(100));
if (lock.owns_lock()) {
cout << "加锁成功" << endl;
} else {
cout << "加锁超时" << endl;
}
}
示例 4:读写锁 shared_mutex(读多写少最优)
cpp
#include <shared_mutex>
shared_mutex s_mtx;
int data = 0;
// 读线程:共享加锁,多个线程可同时读
void read_data() {
shared_lock<shared_mutex> lock(s_mtx);
cout << "读取数据: " << data << endl;
}
// 写线程:独占加锁,同一时间只能一个写
void write_data(int val) {
unique_lock<shared_mutex> lock(s_mtx);
data = val;
cout << "写入数据: " << data << endl;
}
示例 5:多锁防死锁 scoped_lock(C++17)
同时加多个锁,避免死锁:
cpp
mutex m1, m2;
void safe_lock() {
// 一次性加锁 m1 和 m2,自动避免死锁
scoped_lock lock(m1, m2);
}
五、关键区别与选型指南(核心总结)
1. 独占锁 vs 读写锁
- 独占锁(mutex) :读写都互斥,适合读写频率差不多的场景;
- 读写锁(shared_mutex) :读不互斥、写互斥,适合读多写少(性能提升 5~10 倍)。
2. lock_guard vs unique_lock
- lock_guard :轻量、无额外开销,日常开发首选;
- unique_lock:灵活(可手动解锁、超时、配合条件变量),有微小开销。
3. 普通锁 vs 递归锁
- 99% 的场景不要用递归锁,它会掩盖代码设计问题,仅用于递归函数。
六、避坑指南(Orin 开发必看)
- 绝对不要手动调用
lock()/unlock()
异常、提前 return 都会导致锁无法释放,直接死锁。 - 锁粒度尽可能小
只保护共享资源,不要在锁内执行耗时操作(如网络、打印)。 - 避免死锁
- 多锁必须用
scoped_lock同时加锁; - 不要循环等待锁。
- 多锁必须用
- ARM 平台(Orin)优化建议
短临界区(几行代码)优先用 自旋锁 (std::spin_lockC++20),避免线程切换开销。
七、快速选型口诀
- 常规场景 →
mutex + lock_guard - 读多写少 →
shared_mutex + shared_lock - 需要灵活控制/条件变量 →
unique_lock - 多锁防死锁 →
scoped_lock - 递归调用 →
recursive_mutex - 不想永久等待 →
timed_mutex
总结
- C++ 锁分底层互斥体 (5种)和 RAII 管理工具 (4种),永远用 RAII 自动管理;
- 基础开发用
std::mutex + std::lock_guard足够; - 读多写少用读写锁,性能最优;
- Orin(ARM)编译加
-std=c++17 -pthread,代码全平台通用。