当前位置: 首页 > news >正文

C++ 锁

在多线程编程中,当多个线程同时访问共享资源时,可能会导致数据竞争(Data Race),产生不可预期的结果。锁提供了同步机制,确保在同一时间只有一个线程可以访问临界区。

锁的本质是通过互斥机制(Mutual Exclusion)确保:

  • 同一时间只有一个线程能进入访问共享资源的代码段(临界区);
  • 线程对共享资源的修改能被其他线程立即可见(避免 CPU 缓存导致的数据不一致)。

1、标准库锁类型

C++11 及后续标准在<mutex>头文件中提供了多种锁类型,满足不同场景需求。

1.1 std::mutex:基础互斥锁

std::mutex是最基础的互斥锁,提供独占所有权 —— 同一时间仅允许一个线程锁定,其他线程尝试锁定时会阻塞等待,直到锁被释放。

核心操作

  • lock():锁定互斥锁(若已被锁定,当前线程阻塞);
  • unlock():解锁互斥锁(必须由持有锁的线程调用,否则行为未定义);
  • try_lock():尝试锁定(成功返回true,失败返回false,不阻塞)。

基础用法(手动加锁 / 解锁)

#include <iostream>
#include <thread>
#include <mutex>std::mutex mtx; // 全局互斥锁
int shared_value = 0;// 线程函数:对共享变量累加
void increment() {for (int i = 0; i < 10000; ++i) {mtx.lock();       // 手动加锁shared_value++;   // 临界区:安全修改共享资源mtx.unlock();     // 手动解锁(必须执行,否则死锁)}
}int main() {std::thread t1(increment);std::thread t2(increment);t1.join();t2.join();std::cout << "Final value: " << shared_value << std::endl; // 预期20000return 0;
}

手动调用lock()unlock()存在风险 —— 若临界区抛出异常,unlock()可能无法执行,导致锁永远被持有(死锁)。因此,实际开发中严禁手动管理锁的生命周期,应使用 RAII 封装。

1.2 std::lock_guard:RAII 自动管理锁(推荐)

std::lock_guardstd::mutex的 RAII(资源获取即初始化)封装类。其构造函数自动调用lock(),析构函数自动调用unlock(),确保锁在作用域结束时必然释放(即使发生异常)。

特点

  • 不可复制、不可移动(避免锁所有权被意外转移);
  • 生命周期与作用域严格绑定,简单高效。

lock_guard避免手动解锁

void safe_increment() {for (int i = 0; i < 10000; ++i) {// 构造时自动加锁,析构时(离开for循环作用域)自动解锁std::lock_guard<std::mutex> lock(mtx); shared_value++; // 临界区安全执行}
}// 主函数同上,最终输出20000(无死锁风险)

优势:即使shared_value++抛出异常(实际不会),lock_guard的析构函数仍会被调用,确保锁释放。

1.3 std::unique_lock:灵活的 RAII 锁

std::unique_locklock_guard更灵活,支持延迟锁定、手动解锁、所有权转移等操作。其内部维护一个 “是否持有锁” 的状态,因此有额外的性能开销(约几个字节的内存和状态判断)。

核心功能

  • 延迟锁定:构造时不立即加锁(需配合std::defer_lock);
  • 手动控制:可通过lock()unlock()手动加解锁;
  • 所有权转移:可通过std::move()转移锁的所有权(适合传递锁);
  • 尝试锁定:支持try_lock()和超时锁定(try_lock_for()try_lock_until())。

延迟锁定与手动控制

void flexible_operation() {std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 延迟锁定(不立即加锁)// 非临界区操作(无需锁)int temp = 100; lock.lock(); // 手动加锁(进入临界区)shared_value += temp; lock.unlock(); // 提前解锁(退出临界区,让其他线程尽早访问)// 其他非临界区操作
}

超时锁定(避免永久阻塞)

#include <chrono>bool try_operation_with_timeout() {std::unique_lock<std::mutex> lock(mtx, std::defer_lock);// 尝试锁定,最多等待100毫秒if (lock.try_lock_for(std::chrono::milliseconds(100))) {shared_value++;return true; // 锁定成功并执行操作} else {return false; // 超时未获取锁,执行备选逻辑}
}

1.4 std::recursive_mutex:递归锁(同一线程可重入)

std::mutex不允许同一线程重复锁定(会导致死锁),而std::recursive_mutex允许同一线程多次调用lock(),但需对应相同次数的unlock()才能完全释放(内部维护 “递归计数”)。

递归函数中需要重复锁定同一资源(如二叉树遍历中,递归访问节点时需锁定全局计数器)。

递归函数中的锁重入

#include <recursive_mutex>std::recursive_mutex rmtx;
int count = 0;// 递归函数:每次递归都需要锁定
void recursive_count(int depth) {if (depth <= 0) return;std::lock_guard<std::recursive_mutex> lock(rmtx); // 同一线程可多次锁定count++; recursive_count(depth - 1); // 递归调用,再次锁定
}int main() {recursive_count(5);std::cout << "Count: " << count << std::endl; // 输出5(正确累加)return 0;
}

注意:递归锁易掩盖设计缺陷(如过度依赖共享资源),非必要不使用(优先考虑拆分临界区)

1.5 std::shared_mutex(C++17):读写分离锁

std::shared_mutex(或 C++14 的std::shared_timed_mutex)支持两种锁定模式,适用于 “读多写少” 场景:

  • 共享锁(读锁):多个线程可同时获取,用于读取共享资源(不修改);
  • 独占锁(写锁):仅一个线程可获取,用于修改共享资源(此时所有读锁和写锁均阻塞)。

通过分离读写操作,提高读操作的并发性能(读线程间无需互斥)。

读写分离控制

#include <shared_mutex>
#include <vector>std::shared_mutex smtx;
std::vector<int> data = {1, 2, 3}; // 共享数据// 读操作:获取共享锁(允许多线程同时读)
int read_data(int index) {std::shared_lock<std::shared_mutex> lock(smtx); // 共享锁return data[index];
}// 写操作:获取独占锁(仅允许单线程写)
void write_data(int index, int value) {std::unique_lock<std::shared_mutex> lock(smtx); // 独占锁data[index] = value;
}int main() {// 多个读线程可并发执行read_data()std::thread t1([]{ std::cout << read_data(0) << std::endl; });std::thread t2([]{ std::cout << read_data(1) << std::endl; });// 写线程执行时,读线程需等待std::thread t3([]{ write_data(0, 100); });t1.join(); t2.join(); t3.join();return 0;
}

2、死锁

2.1 死锁的产生条件

死锁是指两个或多个线程相互等待对方释放锁,导致永久阻塞的状态,需同时满足:

  • 互斥:锁被线程独占;
  • 持有并等待:线程持有一个锁,同时等待另一个锁;
  • 不可剥夺:线程持有的锁不能被强制释放;
  • 循环等待:线程间形成等待环(如线程 1 等锁 B,线程 2 等锁 A)。

2.2 错误示范

std::mutex mtx_a, mtx_b;// 线程1:先锁A,再等B
void thread1() {std::lock_guard<std::mutex> lock_a(mtx_a);// 模拟临界区操作(给线程2抢锁时间)std::this_thread::sleep_for(std::chrono::milliseconds(10));std::lock_guard<std::mutex> lock_b(mtx_b); // 等待线程2释放B → 死锁
}// 线程2:先锁B,再等A
void thread2() {std::lock_guard<std::mutex> lock_b(mtx_b);std::this_thread::sleep_for(std::chrono::milliseconds(10));std::lock_guard<std::mutex> lock_a(mtx_a); // 等待线程1释放A → 死锁
}int main() {std::thread t1(thread1);std::thread t2(thread2);t1.join(); // 程序永久阻塞(死锁)t2.join();return 0;
}

2.3 避免死锁的核心方法

2.3.1 按固定顺序加锁

所有线程按相同的全局顺序锁定多个锁(如始终先锁mtx_a,再锁mtx_b),打破 “循环等待” 条件。

// 线程1和线程2均按"先A后B"的顺序加锁
void thread1_fixed() {std::lock_guard<std::mutex> lock_a(mtx_a);std::lock_guard<std::mutex> lock_b(mtx_b); // 不会死锁
}void thread2_fixed() {std::lock_guard<std::mutex> lock_a(mtx_a); // 先等A,再锁Bstd::lock_guard<std::mutex> lock_b(mtx_b); 
}
2.3.2 用std::lock原子锁定多个锁

std::lock原子地同时锁定多个互斥量(内部通过复杂逻辑避免死锁),适合无法固定顺序的场景。

void safe_lock_two() {// 原子锁定mtx_a和mtx_b(无顺序依赖)std::lock(mtx_a, mtx_b); // 用adopt_lock标记锁已被锁定,避免重复加锁std::lock_guard<std::mutex> lock_a(mtx_a, std::adopt_lock);std::lock_guard<std::mutex> lock_b(mtx_b, std::adopt_lock);// 临界区操作
}
2.3.3 减少锁的持有时间

临界区仅包含必要操作,锁尽早释放(如用unique_lock手动解锁),降低死锁概率。

3、锁的选择与性能考量

锁类型 核心优势 性能开销 适用场景
std::mutex+lock_guard 简单安全,无额外开销 大多数场景,临界区简单且短
std::unique_lock 支持延迟锁定、超时、所有权转移 需要灵活控制锁生命周期的场景
std::recursive_mutex 允许同一线程重入 中高 递归函数需重复锁同一资源(谨慎使用)
std::shared_mutex 读写分离,提高读并发 读多写少,如缓存、配置数据访问

C++ 锁通过互斥机制解决多线程共享资源的竞态条件问题,其核心是确保临界区原子性。实际开发中:

  1. 优先使用 RAII 封装(lock_guard/unique_lock)避免手动管理锁的风险;
  2. 根据场景选择锁类型(如读多写少用shared_mutex);
  3. 通过固定加锁顺序、std::lock等方法严格避免死锁。
http://www.hskmm.com/?act=detail&tid=14783

相关文章:

  • 飞书对程序员下手了,0 代码生成各类系统!!(附保姆级项目实战教程)
  • Adaptix C2:跨平台渗透测试与对抗仿真框架
  • 国标GB28181软件EasyGBS网页直播平台在邮政快递场景的落地与应用
  • sql统计一个字段各个值各有多个个的方法
  • WBS、甘特图、关键路径……项目计划的五大核心概念一文全懂
  • 智启新程:哲讯科技引领SAP ERP实施新范式
  • 移动端性能监控探索:鸿蒙 NEXT 探针架构与技术实现
  • 哲讯科技:以数智之力,铸就企业SAP ERP实施新典范
  • PR曲线绘制
  • 5台电脑怎么同步文件最安全高效?别再只知道用局域网共享了!
  • 关于CompatibilityHID例程的使用
  • SystemVerilog 代码风格指南
  • 赋能智慧化工:无锡哲讯科技SAP解决方案,构筑安全、合规与高效的数字新底座
  • 芯之所向,智造未来:无锡哲讯科技赋能芯片行业的高效管理与数字革新
  • UART、I2C、SPI:三种常见通信协议的区别
  • Day05---数据类型的转换
  • 个人项目——论文查重
  • 效率党的图片处理新选择:滴答修——在线全能工具箱,免费且强大
  • GPU0与GPU1
  • 对接全球股票市场K线数据实战
  • 9.23
  • centos安装docker和Jenkins
  • 硬件检测神器 HWiNFO:全组件监控 + 多系统兼容,免费无广告,运维 / 评测必备
  • Qt - 音频采集程序
  • 923-
  • 基于 AI 网关提升大模型应用可用性的实践
  • 绝了!TaskMatrix Pro - 谷歌、火狐浏览器任务管理插件,四象限矩阵让拖延症瞬间消失 - 开源免费
  • 洛谷P10288 [GESP样题 八级] 区间
  • AI 时代下,开发流程的重塑:从“代码先行”到“文档驱动”
  • P13617 [ICPC 2025 APC] Bit Counting Sequenc