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

C++11并发编程:互斥锁

承接上一篇学习的std::thread,这篇搞定多线程数据共享+各种类互斥锁+RAII锁管理

1:什么是数据竞争

1:数据竞争

多个线程同时读写同一个共享全局 / 堆变量,且没有任何同步保护,就会产生数据竞争,结果未定义、数值错乱。

示例(无锁错误代码)

#include <iostream> #include <thread> using namespace std; int g_cnt = 0; void add() { for (int i = 0; i < 1000000; ++i) { g_cnt++; } } int main() { thread t1(add); thread t2(add); t1.join(); t2.join(); // 理论应该 2000000,实际永远小于这个数 cout << g_cnt << endl; return 0; }

原因:

g_cnt++不是原子操作,拆成三步:读内存 → 寄存器自增 → 写回内存,多线程穿插执行就会覆盖丢失。

2:临界区和互斥锁

  • 临界区:访问共享数据的代码片段
  • 互斥锁 mutex:保证同一时刻只有一个线程进入临界区,其他线程阻塞等待。

2:C++11四种常见互斥锁分类

锁类型特点适用场景
std::mutex普通互斥锁、不可递归、非定时常规业务临界区,最常用
std::recursive_mutex可递归加锁,同一线程可多次 lock递归函数、类成员函数嵌套加锁
std::timed_mutex普通锁 + 超时等待不想死等,等待一段时间拿不到锁就放弃
std::recursive_timed_mutex可递归 + 超时递归且需要超时控制
  • 一个线程 lock 成功后,其他线程 lock 阻塞、try_lock 返回 false
  • 锁没解锁就销毁 / 持有锁线程直接退出 → 行为未定义

3:std::mutex基础用法

1:核心接口

void lock(); // 加锁,拿不到就阻塞 void unlock(); // 解锁 bool try_lock(); // 尝试加锁,拿到返回true,拿不到立刻返回false不阻塞

2:基础加锁解锁示例

#include <iostream> #include <thread> #include <mutex> using namespace std; int g_cnt = 0; mutex mtx; void add() { for (int i = 0; i < 1000000; ++i) { mtx.lock(); g_cnt++; mtx.unlock(); } } int main() { thread t1(add); thread t2(add); t1.join(); t2.join(); cout << g_cnt << endl; // 正常 2000000 return 0; }

3:手写lock/unlock的致命问题

  1. 中途抛异常,unlock 没执行 → 死锁
  2. 代码分支多,容易漏写 unlock
  3. 可读性差,成对维护麻烦

所以必须用RAII 锁管理:lock_guard /unique_lock

4:std::lock_guard简洁RAII自动锁

1:原理

RAII 思想:

  • 构造时自动 lock
  • 离开作用域析构时自动 unlock
  • 不用手动写 lock /unlock,异常也能自动解锁

2:构造方式

// 1. 常规加锁:构造立刻lock explicit lock_guard(mutex_type& m); // 2. 接管已经加好的锁:adopt_lock lock_guard(mutex_type& m, adopt_lock_t tag);

3:基础使用

void add() { for (int i = 0; i < 1000000; ++i) { lock_guard<mutex> lg(mtx); // 临界区 g_cnt++; // 离开作用域 lg 析构,自动 unlock } }

4:adopt_lock

场景:已经手动 lock 了,交给 lock_guard 接管,析构自动解锁

void print_id(int id) { mtx.lock(); lock_guard<mutex> lck(mtx, adopt_lock); cout << "thread #" << id << endl; // 不用手动unlock,出作用域自动解 }

5:lock_guard限制

  • 不能拷贝、不能移动
  • 不能手动解锁、不能延迟加锁
  • 功能极简,只适合简单固定临界区

5:std::recursive_mutex递归互斥锁

1:解决什么问题

普通std::mutex同一线程重复 lock 直接死锁。recursive_mutex 允许同一个线程多次加锁,解锁次数要和加锁次数匹配。

2:适用场景

  • 递归函数内部加锁
  • 类多个成员函数都要加锁,互相调用嵌套

3:示例

recursive_mutex rmtx; void funcA() { rmtx.lock(); cout << "funcA" << endl; funcB(); rmtx.unlock(); } void funcB() { rmtx.lock(); // 同一线程递归加锁,不会阻塞 cout << "funcB" << endl; rmtx.unlock(); }

建议:尽量少用递归锁,能重构代码就重构,递归锁性能略差、容易隐藏逻辑问题。

6:std::timed_mutex带超时互斥锁

1:新增接口

// 尝试加锁,阻塞指定时长,超时拿不到返回false bool try_lock_for(chrono::duration); // 阻塞到某个时间点,超时返回false bool try_lock_until(chrono::time_point);

2:经典事例

每隔 200ms 打印-,最多等 1 秒拿锁:

#include <iostream> #include <thread> #include <mutex> #include <chrono> using namespace std; timed_mutex mtx; void fireworks(int i) { // 最多等待1秒,拿不到就循环打 "-" while (!mtx.try_lock_for(chrono::milliseconds(1000))) { cout << "-"; } // 拿到锁 cout << i; this_thread::sleep_for(chrono::milliseconds(5000)); cout << "*\n"; mtx.unlock(); } int main() { thread threads[2]; for (int i = 0; i < 2; ++i) threads[i] = thread(fireworks, i); for (auto& th : threads) th.join(); return 0; }

7:std::unique_lock:功能最强的RAII锁

1:为什么有了lock_guard还要unique_lock

lock_guard 太死板,unique_lock 支持:

  • 延迟加锁
  • 尝试加锁
  • 超时加锁
  • 手动 lock/unlock
  • 支持移动语义
  • 可以配合条件变量condition_variable(必须用 unique_lock)

2:七种构造方式

构造方式含义
无参空锁,不绑定任何 mutex
传 mutex构造直接加锁
try_to_lock构造尝试加锁,不阻塞
defer_lock延迟加锁,构造不加,后面手动 lock
adopt_lock接管已经加好的锁
时间段try_lock_for 超时等待
时间点try_lock_until 超时等待

3:常用示例

1:延迟加锁defer_lock
mutex mtx; void test() { unique_lock<mutex> lk(mtx, defer_lock); // 做一些无关操作... lk.lock(); // 手动加锁 // 临界区 lk.unlock(); // 手动解锁 // 还可以再加锁 lk.lock(); }
2:支持移动不支持拷贝
unique_lock<mutex> lk1(mtx); unique_lock<mutex> lk2 = move(lk1); // 移动可以 // unique_lock<mutex> lk3 = lk1; // 拷贝禁用
3:配合条件变量使用

后面讲 condition_variable 会用到:条件变量 wait 只能接收 unique_lock,不能用 lock_guard。

4:lock_guard VS unique_lock

  • 简单临界区、不需要手动解锁、不需要条件变量 →lock_guard轻量
  • 需要延迟加锁、手动解锁、超时、移动、条件变量 →unique_lock

8:多锁同时加锁std::lock/std::try_lock

1:std::lock模版函数

一次性锁住多个互斥锁避免死锁:内部会自动处理加锁顺序,若部分锁住、部分没锁住,会先释放已锁住的,再阻塞等待全部拿到。

mutex m1, m2; void taskA() { lock(m1, m2); // 同时锁两个,不会死锁 // 临界区 m1.unlock(); m2.unlock(); } void taskB() { lock(m2, m1); // 颠倒顺序也没事,std::lock内部规避死锁 }

2:std::try_lock

尝试一次性锁多个:

  • 全部成功返回-1
  • 某个失败,返回失败的下标,并释放已拿到的锁

9:thread传参为什么必须用std::ref

template <class _Fn, class... _Args, enable_if_t<!is_same_v<_Remove_cvref_t<_Fn>, thread>, int> = 0> _NODISCARD_CTOR explicit thread(_Fn &&_Fx, _Args &&..._Ax) { _Start(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...); } template <class _Fn, class... _Args> void _Start(_Fn &&_Fx, _Args &&..._Ax) { // 从下⾯可以看到,线程要调⽤系统库的线程,最终还是要把参数包打包成⼀个结构体对象再传给线程,所以线程中拿到的参数包值是我们传的参数包值的拷⻉,所以要⽤ref才传参才能解决问题 using _Tuple = tuple<decay_t<_Fn>, decay_t<_Args>...>; auto _Decay_copied = _STD make_unique<_Tuple>(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...); constexpr auto _Invoker_proc = _Get_invoke<_Tuple>(make_index_sequence<1 + sizeof...(_Args)>{}); // pointer or reference to potentially throwing function passed to // extern C function under -EHc. Undefined behavior may occur // if this function throws an exception. (/Wall) _Thr._Hnd = reinterpret_cast<void *>(_CSTD _beginthreadex(nullptr, 0, _Invoker_proc, _Decay_copied.get(), 0, &_Thr._Id)); if (_Thr._Hnd) { // ownership transferred to the thread (void)_Decay_copied.release(); } else { // failed to start thread _Thr._Id = 0; _Throw_Cpp_error(_RESOURCE_UNAVAILABLE_TRY_AGAIN); } }

线程底层会把所有参数打包成 tuple 做值拷贝传给系统线程入口。如果你直接传普通变量,线程里拿到的是拷贝副本,修改不影响外部。

想要真正传引用:必须用std::ref()/std::cref()包装,让 tuple 推导为引用类型。

示例标准写法:

void func(int& x, mutex& mtx) { lock_guard<mutex> lg(mtx); x++; } int main() { int x = 0; mutex mtx; // 必须ref,否则编译报错 / 传值副本 thread t(func, ref(x), ref(mtx)); t.join(); return 0; }

替代方案:用 lambda 捕获引用,不用传参,更优雅:

thread t([&x, &mtx](){ lock_guard<mutex> lg(mtx); x++; });

10:总结

  • 多线程共享变量必有数据竞争,必须用互斥锁保护临界区;
  • std::mutex基础锁,不可递归;recursive_mutex支持同线程多次加锁;
  • timed_mutex带超时等待,避免无限阻塞;
  • lock_guard极简 RAII,自动加解锁,性能好,功能单一;
  • unique_lock功能最全,支持延迟 / 尝试 / 手动加解锁、移动、适配条件变量;
  • std::lock/try_lock一次性多锁,解决死锁问题;
  • 线程传引用必须std::ref,或 lambda 捕获引用更省事。
http://www.zskr.cn/news/1439049.html

相关文章:

  • 告别环境配置焦虑:MacBook M系列芯片(Apple Silicon)Java开发环境一键式配置心得
  • 别再手动建模了!用SolidWorks+Simulink搞机械仿真,保姆级插件安装与配置避坑指南
  • 小米MiMo-7B-MTPs震撼发布:解锁语言模型推理潜能的终极解决方案
  • OpenEuler欧拉系统X86版YUM源配置保姆级教程(含离线/内网场景解决方案)
  • 社交自动上传神器的时间管理秘籍:files_times.py智能时间戳处理指南
  • Xverse:自动化混合特征选择工具,轻松应对维度灾难
  • 告别视频拖影!手把手教你用Python+OpenCV实现一个简易的时空联合3D降噪器
  • 鸿蒙 地图开发:标记(Marker)增加
  • 如何永久保存微信聊天记录?开源工具WeChatMsg的终极备份指南
  • 如何快速部署Dmeta-embedding-zh:免费商用的中文文本嵌入模型完整指南 [特殊字符]
  • 面试官追问的Python‘八股文’,我用一个爬虫项目全讲清楚了(附避坑指南)
  • AI文档管理:从智能分类到自动化提取的7大核心优势
  • Instant-NGP 实战:用多分辨率哈希编码,5分钟让你的NeRF训练快100倍
  • 【教学类-160-43】20260524 AI视频培训-练习043“豆包AI视频《三字经》片段(演唱:04ZXY)+豆包图片风格:卡通
  • FOC 电流环PI 速度环PI
  • 基于边缘计算与Cloudflare Workers构建个人新闻聚合系统
  • 当AI学会了自己写代码:深入拆解OpenAI Codex CLI的Rust架构设计与工程哲学
  • 别再死记硬背了!用购物车和订单系统实战,5分钟搞懂UML类图的6种关系
  • LFM2.5-VL-450M WebGPU实时视频流字幕生成:浏览器端视觉AI应用的完整指南 [特殊字符]
  • Vue项目实战:用vue3-scroll-seamless为数据大屏打造‘会呼吸’的实时滚动列表
  • PCB设计省钱指南:如何用SI9000仿真帮你选对板材(FR4还是高速料?)
  • 双端口构网控制技术在混合交直流系统中的应用
  • 保姆级教程:用Nvidia-smi命令行参数,给你的GPU做个‘全身体检’
  • AI驱动招聘自动化:从简历解析到智能匹配的实战架构与落地
  • Spring Boot 从零入门:请求响应、三层架构与 IOC/DI 实践总结
  • openEuler内网yum源搭建实战:用Nginx快速部署,实现团队共享软件包
  • Rust服务端渲染实战:集成Dall.E API构建高性能AI图像生成应用
  • 拒绝“胡言乱语”:企业级 RAG 应用中如何彻底规避 LLM 幻觉?
  • SharePoint 反序列化漏洞拿下 CVSS 8.8 + Windows 内核提权:五月高危漏洞集中爆发,服务器防护还有哪些盲区
  • 告别Resources文件夹!用Unity Addressables 1.19.19管理你的游戏资源,附完整避坑指南