当前位置:

首页 > 编程开发 > C++中锁的种类与使用实例详解

C++中锁的种类与使用实例详解

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

关于C++中锁的种类与使用

在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/unlocklock_guard
正常执行✓ 释放✓ 自动释放
异常抛出锁泄露✓ 析构仍会释放
逻辑分支✗ 容易遗漏✓ 自动保证

可以说,是解决多线程数据竞争的核心工具。从C++11开始,标准库就提供了完善的锁机制。对于你正在使用的NVIDIA Orin平台(Ubuntu 20.04,ARM架构),这些标准锁都能直接使用,编译时只需记得链接线程库(-pthread)。

接下来,我们将从底层互斥体(锁的本体)RAII自动锁管理工具(强烈推荐)两部分入手,系统梳理它们的种类、特性、区别、适用场景,并附上可直接运行的代码示例,最后还会分享一些关键的避坑经验。

一、前置核心知识

  1. 作用:保护共享资源(如变量、文件、硬件接口),确保同一时间只有一个或指定类型的线程可以访问,从而避免数据错乱。
  2. 核心头文件
    #include         // C++11 基础锁
    #include  // C++17 读写锁
    #include        // 线程
    
  3. 黄金原则务必使用RAII自动管理锁。手动调用lock()/unlock()极易因忘记解锁而导致死锁。
  4. 编译命令(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(超时独占锁)

  • 特性:支持超时尝试加锁,线程不会因获取不到锁而永久阻塞。
  • APItry_lock_for(时间)try_lock_until(时间点)
  • 适用场景:需要避免无限期等待锁的场景,例如硬件通信、带超时的任务处理。

4. std::shared_mutex(读写锁 / 共享互斥锁,C++17)

  • 核心特性:实现了读写分离
    • 读模式(共享):多个线程可以同时加锁读取数据;
    • 写模式(独占):只有一个线程能加锁写入数据,写入时禁止任何读取。
  • 适用场景读多写少的场景,如配置读取、日志系统、缓存等。在这种场景下,其性能通常远高于普通的独占锁。

三、第二部分:RAII自动锁管理工具(必用!)

直接操作底层互斥体容易出错,因此C++提供了几种自动锁管理工具。它们遵循RAII原则:在构造时加锁,在析构时自动解锁

管理工具配合锁类型手动解锁超时支持多锁同时加锁核心用途
std::lock_guard所有独占锁最简单、最常用(默认首选)
std::unique_lock所有独占锁灵活锁(配合条件变量、手动控制)
std::shared_lockshared_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

解决递归函数内部的加锁问题:

#include 
recursive_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(读多写少最优)

#include 
shared_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开发必看)

  1. 绝对不要手动调用 lock()/unlock()

    一旦代码中抛出异常或提前return,锁就可能无法释放,直接导致死锁。

  2. 锁粒度尽可能小

    锁只应用于保护共享资源本身,应避免在锁的范围内执行耗时操作,如网络I/O或大量控制台打印。

  3. 避免死锁
    • 当需要获取多个锁时,务必使用 scoped_lock 一次性获取;
    • 注意避免线程间形成循环等待锁的局面。
  4. ARM平台(Orin)优化建议

    对于临界区非常短(只有几行代码)的场景,可以优先考虑使用自旋锁(如C++20的std::spin_lock),这能避免线程切换的开销,提升性能。当然,这需要评估临界区的实际长度。

七、快速选型口诀

  1. 常规场景 → mutex + lock_guard
  2. 读多写少 → shared_mutex + shared_lock
  3. 需要灵活控制/条件变量 → unique_lock
  4. 多锁防死锁 → scoped_lock
  5. 递归调用 → recursive_mutex
  6. 不想永久等待 → timed_mutex

总结

  1. C++的锁分为底层互斥体(5种)和RAII管理工具(4种),始终坚持使用RAII进行自动管理
  2. 基础开发使用 std::mutex + std::lock_guard 组合通常就足够了;
  3. 在读操作远多于写操作的场景下,使用读写锁能获得最优性能;
  4. 在Orin(ARM)平台上编译时,记住加上 -std=c++17 -pthread 选项,这些代码是跨平台通用的。
本文内容来源于互联网,如有侵权请联系删除。
作者最新文章
编程开发
相关文章 更多
C++动态数组初始化怎么写?常用语句与代码示例
C++动态数组初始化怎么写?常用语句与代码示例

深入解析C++中动态数组的初始化机制,涵盖new操作符的不同用法、基本类型与类对象的初始化差异,以及为何在现代C++开发中应优先使用std::vector。

using namespace 使用中遇到的问题怎么解决
using namespace 使用中遇到的问题怎么解决

命名空间的基本概念与常见引入问题在C++等编程语言中,命名空间(namespace)是一种将代码标识符(如变量、函数、类名)封装在特定名称下的机制,其主要目的是避免命名冲突,尤其是在大型项目或使用多个第三方库时。使用“using namespace”指令可以将指定命名空间中的所有名称引入当前作用域,

c语言函数递归 实操经验总结:这些技巧很实用
c语言函数递归 实操经验总结:这些技巧很实用

理解递归的基本原理在C语言中,递归是一种函数调用自身的编程技术。要掌握它,首先需要理解其核心思想:将一个复杂的大问题,分解为一个或几个与原问题相似但规模更小的子问题,直到子问题足够简单,可以直接求解。这个过程通常包含两个关键部分:递归出口和递归体。递归出口定义了问题何时不再继续分解,即最简单、可直接

c语言函数递归 怎么选?常见方案对比分析
c语言函数递归 怎么选?常见方案对比分析

递归函数的基本概念与适用场景在C语言编程中,递归是一种函数调用自身的编程技巧。它并非适用于所有问题,但在处理某些具有自相似结构的问题时,能提供极其清晰和优雅的解决方案。递归的核心思想是将一个大规模问题分解为一个或多个同类型但规模更小的子问题,直到子问题简单到可以直接求解。典型的适用场景包括树形结构的

Objective-C 内存管理入门:从 alloc 到 dealloc 的生命周期详解
Objective-C 内存管理入门:从 alloc 到 dealloc 的生命周期详解

理解内存管理的基石在Objective-C的编程世界中,内存管理是开发者必须掌握的核心技能之一。它直接关系到应用的性能、稳定性与资源利用效率。与一些采用自动垃圾回收机制的语言不同,Objective-C在很长一段时间里,依赖一套基于引用计数的、需要开发者部分介入的管理规则。这套规则的核心思想是明确的

如何正确使用 dealloc 以避免 iOS 应用中的内存泄漏
如何正确使用 dealloc 以避免 iOS 应用中的内存泄漏

理解 dealloc 的角色与时机在 iOS 应用开发中,内存管理是保障应用性能与稳定性的基石。dealloc 方法是 Objective-C 中对象生命周期结束时的关键回调,它标志着对象即将被系统回收内存。正确理解其触发时机至关重要:当一个对象的引用计数降为零时,运行时系统会自动调用该对象的 de

深入理解 Objective-C 中的 dealloc 方法:内存管理核心机制
深入理解 Objective-C 中的 dealloc 方法:内存管理核心机制

内存管理的基石在Objective-C的世界里,内存管理是开发者必须掌握的核心技能之一。作为一门在手动引用计数(MRC)时代诞生的语言,Objective-C要求程序员对对象的生命周期有清晰的认识。dealloc方法正是这一生命周期中至关重要的终点站。它是一个实例方法,当对象的引用计数降为零时,系统

理解 native2ascii:Java 国际化开发中的字符编码工具
理解 native2ascii:Java 国际化开发中的字符编码工具

native2ascii 工具的基本定位在Ja va应用程序的国际化与本地化开发过程中,处理非拉丁字符集是一个常见且关键的环节。Ja va内部使用Unicode字符集来统一表示全球各种语言的文字,但其属性文件(.properties)在历史上要求使用ASCII编码,或者更准确地说,要求非ASCII字

如何使用 native2ascii 转换中文字符为 Unicode 转义序列
如何使用 native2ascii 转换中文字符为 Unicode 转义序列

理解 native2ascii 工具的基本用途在软件开发,特别是涉及国际化处理的场景中,开发者常常需要处理不同编码的文本资源。native2ascii 是 Ja va 开发工具包(JDK)中提供的一个命令行实用程序,其主要功能是将包含本地字符编码(非ASCII字符)的文件,转换为包含 Unicode

Java native2ascii 命令详解:解决属性文件乱码问题
Java native2ascii 命令详解:解决属性文件乱码问题

native2ascii 命令的由来与作用在Ja va开发中,处理国际化资源文件是一个常见需求。资源文件通常以.properties格式存储,用于支持多语言界面。然而,Ja va属性文件默认采用ISO-8859-1字符集编码,这导致了一个直接的问题:当文件中包含非拉丁字符(如中文、日文、韩文等)时,

查看更多
精品专题 更多
装机必备
装机必备

正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

Windows
Windows

正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

macOS软件
macOS软件

正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

Mac软件 更多
灵活计算器
灵活计算器
macOS/iOS/Android

灵活计算器是一款笔记式算数应用,支持实时计算、动态关联和云端同步功能。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

赤友清理大师
赤友清理大师
macOS

赤友清理大师是一款为 Mac 设计的智能清理优化工具,可精准扫描垃圾、大文件、重复文件等,释放磁盘空间。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

极度公式
极度公式
Windows/macOS/Linux

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

WINDOWS 更多
Windows 10
Windows 10
Windows

Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。

极度公式
极度公式
Windows/macOS/Linux

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

密码键盘
密码键盘
Windows/macOS/iOS/Android

密码键盘是一款兼具安全性与便捷性的高效密码管理器。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。