C++中锁的种类与使用实例详解
C++多线程编程中,锁是解决数据竞争的核心工具。标准库提供了多种锁机制,包括基础独占锁、递归锁、超时锁和读写锁等。推荐使用遵循RAII原则的自动锁管理工具,如lock_guard,以自动管理锁的生命周期,避免手动解锁导致的死锁或资源泄露。这些机制能有效保障程序的稳定性和性能。
关于C++中锁的种类与使用
在C++多线程编程中,选择合适的锁管理机制,往往是决定程序稳定性和性能的关键一步。标准库提供了多种工具,各有其适用的场景和特性。

来看一个典型的例子。这段代码里使用的是 std::lock_guard,它是一个自动管理锁生命周期的工具。那么,锁究竟在什么时候被释放呢?
锁的生命周期
.....
static std::mutex g_mutex; // 保护 g_control / g_reply 的并发访问
.....
void handleControlGram(const ControlGram &cg) {
std::lock_guard lk(g_mutex); // ← 构造时自动加锁
g_control.SteeringThrottle = (uint8_t)cg.SteeringThrottle;
// ... 其他赋值操作
g_control.ControlMode = cg.ControlMode;
} // ← 函数结束时,lk 对象析构,锁自动释放
锁的释放时机:
- 函数返回时 —
lock_guard对象超出作用域,析构函数被调用,锁自动释放 - 整个过程无需手动调用
unlock()
为什么首选 lock_guard?
这背后是经典的 RAII 模式(资源获取即初始化)在发挥作用。它带来的好处,通过一个简单的对比就能一目了然:
| 情形 | 手动 lock/unlock | lock_guard |
|---|---|---|
| 正常执行 | ✓ 释放 | ✓ 自动释放 |
| 异常抛出 | ✗ 锁泄露 | ✓ 析构仍会释放 |
| 逻辑分支 | ✗ 容易遗漏 | ✓ 自动保证 |
可以说,锁是解决多线程数据竞争的核心工具。从C++11开始,标准库就提供了完善的锁机制。对于你正在使用的NVIDIA Orin平台(Ubuntu 20.04,ARM架构),这些标准锁都能直接使用,编译时只需记得链接线程库(-pthread)。
接下来,我们将从底层互斥体(锁的本体)和RAII自动锁管理工具(强烈推荐)两部分入手,系统梳理它们的种类、特性、区别、适用场景,并附上可直接运行的代码示例,最后还会分享一些关键的避坑经验。
一、前置核心知识
- 作用:保护共享资源(如变量、文件、硬件接口),确保同一时间只有一个或指定类型的线程可以访问,从而避免数据错乱。
- 核心头文件:
#include
// C++11 基础锁 #include // C++17 读写锁 #include // 线程 - 黄金原则:务必使用RAII自动管理锁。手动调用
lock()/unlock()极易因忘记解锁而导致死锁。 - 编译命令(Orin通用):
g++ -std=c++17 代码.cpp -o 程序 -pthread
二、第一部分:C++标准底层互斥体(锁的本体)
这是锁的核心实现,主要分为独占锁、递归锁、超时锁、读写锁四大类,共计五种:
| 互斥体类型 | 独占性 | 递归 | 超时 | 共享读写 | 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++提供了几种自动锁管理工具。它们遵循RAII原则:在构造时加锁,在析构时自动解锁。
| 管理工具 | 配合锁类型 | 手动解锁 | 超时支持 | 多锁同时加锁 | 核心用途 |
|---|---|---|---|---|---|
std::lock_guard | 所有独占锁 | ❌ | ❌ | ❌ | 最简单、最常用(默认首选) |
std::unique_lock | 所有独占锁 | ✅ | ✅ | ❌ | 灵活锁(配合条件变量、手动控制) |
std::shared_lock | shared_mutex | ✅ | ✅ | ❌ | 读写锁的读模式(共享) |
std::scoped_lock | 所有锁 | ❌ | ❌ | ✅ | C++17 同时锁多个锁,防死锁 |
四、完整使用示例(可直接在Orin上运行)
示例1:基础用法 std::mutex + lock_guard(首选)
最常用的组合,适用于保护简单的共享变量:
#include#include #include using namespace std; mutex mtx; // 全局互斥锁 int share_num = 0; // 共享资源 void add_num() { // 构造时自动加锁,离开作用域自动解锁 lock_guard 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
解决递归函数内部的加锁问题:
#includerecursive_mutex rmtx; void func(int n) { lock_guard lock(rmtx); if (n > 0) func(n-1); // 同一线程内递归调用,重复加锁不会死锁 }
示例3:超时锁 timed_mutex + unique_lock
避免线程因锁而永久阻塞:
#include#include timed_mutex tmtx; void try_lock_func() { // 尝试加锁,最多等待 100ms unique_lock lock(tmtx, chrono::milliseconds(100)); if (lock.owns_lock()) { cout << "加锁成功" << endl; } else { cout << "加锁超时" << endl; } }
示例4:读写锁 shared_mutex(读多写少最优)
#includeshared_mutex s_mtx; int data = 0; // 读线程:共享加锁,多个线程可同时读 void read_data() { shared_lock lock(s_mtx); cout << "读取数据: " << data << endl; } // 写线程:独占加锁,同一时间只能一个写 void write_data(int val) { unique_lock lock(s_mtx); data = val; cout << "写入数据: " << data << endl; }
示例5:多锁防死锁 scoped_lock(C++17)
需要同时获取多个锁时,避免死锁的利器:
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 递归锁
- 绝大多数场景下应避免使用递归锁。它的存在往往是为了解决特定设计问题(如递归函数),而非最佳实践。
六、避坑指南(Orin开发必看)
- 绝对不要手动调用
lock()/unlock()一旦代码中抛出异常或提前return,锁就可能无法释放,直接导致死锁。
- 锁粒度尽可能小
锁只应用于保护共享资源本身,应避免在锁的范围内执行耗时操作,如网络I/O或大量控制台打印。
- 避免死锁
- 当需要获取多个锁时,务必使用
scoped_lock一次性获取; - 注意避免线程间形成循环等待锁的局面。
- 当需要获取多个锁时,务必使用
- ARM平台(Orin)优化建议
对于临界区非常短(只有几行代码)的场景,可以优先考虑使用自旋锁(如C++20的
std::spin_lock),这能避免线程切换的开销,提升性能。当然,这需要评估临界区的实际长度。
七、快速选型口诀
- 常规场景 →
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选项,这些代码是跨平台通用的。
Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。
极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。
















