「硬核科普」C++11锁机制三兄弟大比拼:mutex、lock_guard与unique_lock
大家好啊,我是小康。今天咱们聊点"家常"——那些让C++程序员又爱又恨的多线程同步工具!
如果你曾经被多线程搞得头大,或者听到"死锁"就心慌,那这篇文章就是为你准备的。今天我要用最接地气的方式,帮你彻底搞懂C++11中的三兄弟:mutex
、lock_guard
和unique_lock
。
为啥要用这些同步工具?
先别急着学怎么用,咱们得先知道为啥要用啊!
想象一下:你和室友共用一个卫生间。如果你们同时冲进去...嗯,画面太美不敢想象。所以你们会怎么做?肯定是先看看有没有人,没人才进去,然后反锁门,用完了再开门。
多线程程序也一样!不同的线程可能会同时访问同一块"地盘"(共享资源),如果不加控制,就会出现数据错乱、程序崩溃等一系列灾难。
这时候,我们的三兄弟就闪亮登场了!
老大:mutex(互斥锁)
mutex
就像那个卫生间的门锁,它是最基础的同步工具,核心功能就两个:锁上(lock
)和开锁(unlock
)。
来看个最简单的例子:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // 这就是我们的"门锁"
int shared_value = 0; // 这是我们要保护的"卫生间"
void increment_value() {
mtx.lock(); // 进去之前先锁门
std::cout << "线程 " << std::this_thread::get_id() << " 进入临界区" << std::endl;
// 想象这是个很复杂的操作,需要一些时间
shared_value++;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::cout << "线程 " << std::this_thread::get_id() << " 即将离开,共享值为: " << shared_value << std::endl;
mtx.unlock(); // 用完了记得开锁,让别人能进来
}
int main() {
std::thread t1(increment_value);
std::thread t2(increment_value);
t1.join();
t2.join();
return 0;
}
看着挺简单对吧?但这有个大坑——如果在lock
和unlock
之间发生了异常,或者你单纯忘记了unlock
,那么锁就永远不会被释放,其他线程永远进不了"卫生间"!这就是传说中的"死锁"。
正因如此,直接使用mutex
很容易出错,所以C++11给我们提供了更智能的解决方案。
老二:lock_guard(保安大哥)
lock_guard
就像一个靠谱的保安大哥。当你进"卫生间"时,他会自动锁门;当你出来时,无论是正常出来还是因为突发情况(异常)跑出来,他都会负责解锁。
看看用lock_guard
如何改写上面的例子:
void safer_increment() {
std::lock_guard<std::mutex> guard(mtx); // 保安上岗,自动锁门
std::cout << "线程 " << std::this_thread::get_id() << " 进入临界区" << std::endl;
// 即使这里抛出异常,离开函数作用域时lock_guard也会自动解锁
shared_value++;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::cout << "线程 " << std::this_thread::get_id() << " 即将离开,共享值为: " << shared_value << std::endl;
// 不需要手动解锁,guard离开作用域时会自动解锁
}
是不是简单多了?这就是RAII(资源获取即初始化)的魅力——资源的管理跟对象的生命周期绑定在一起。lock_guard
一旦创建就会锁定互斥量,一旦销毁(离开作用域)就会解锁互斥量。
不过lock_guard
有个局限性:一旦上锁,在其生命周期内你就不能手动解锁了。就像你请了个特别死板的保安,他坚持要等你彻底离开才会开门,中途想出去透个气都不行。
老三:unique_lock(万能管家)
如果说lock_guard
是保安大哥,那unique_lock
就是一个高级管家,不但能自动锁门解锁,还能根据你的指令随时锁门或开门,甚至可以"借"钥匙给别人。
来看个例子:
void flexible_operation() {
std::unique_lock<std::mutex> superlock(mtx); // 默认情况下构造时会锁定mutex
std::cout << "线程 " << std::this_thread::get_id() << " 开始工作" << std::endl;
shared_value++;
// 假设这里不需要锁了,可以提前解锁
superlock.unlock();
std::cout << "临时解锁,执行一些不需要保护的操作" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(50));
// 需要再次访问共享资源时,可以重新上锁
superlock.lock();
shared_value++;
std::cout << "线程 " << std::this_thread::get_id() << " 完成工作,共享值为: " << shared_value << std::endl;
// 同样,不需要手动解锁,离开作用域时会自动解锁(如果当时处于锁定状态)
}
除了手动lock
和unlock
,unique_lock
还有更多高级功能:
std::unique_lock<std::mutex> master_lock(mtx, std::defer_lock); // 创建时不锁定
if (master_lock.try_lock()) { // 尝试锁定,如果失败也不会阻塞
std::cout << "成功获取锁!" << std::endl;
} else {
std::cout << "获取锁失败,但我可以去做别的事" << std::endl;
}
// 还可以配合条件变量使用
std::condition_variable cv;
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; }); // 这里会自动解锁并等待条件满足
unique_lock
比lock_guard
灵活,但也付出了一点性能代价,它内部需要维护更多状态信息。
三兄弟大比拼
说了这么多,来个简单对比:
特性 | mutex | lock_guard | unique_lock |
---|---|---|---|
手动锁定/解锁 | |||
异常安全 | (需手动保证) | ||
条件变量配合 | |||
尝试锁定(try_lock) | |||
性能开销 | 最小 | 很小 | 稍大 |
使用难度 | 容易出错 | 简单安全 | 灵活但复杂 |
实战:模拟ATM取款与系统维护
最后用一个贴近生活的例子来巩固一下。假设我们有个ATM系统,既要处理用户取款,又要处理银行的系统维护:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
class ATMSystem {
private:
double cash_available; // ATM中可用现金
bool maintenance_mode; // 是否处于维护模式
std::mutex mtx;
std::condition_variable cv; // 条件变量,用于等待维护结束
public:
ATMSystem(double initial_cash) : cash_available(initial_cash), maintenance_mode(false) {}
// 用户取款操作
bool withdraw(double amount) {
// 这里必须用unique_lock,因为条件变量wait需要它
std::unique_lock<std::mutex> lock(mtx);
// 如果ATM正在维护中,等待维护结束
cv.wait(lock, [this] { return !maintenance_mode; });
// 检查余额并取款
if (cash_available >= amount) {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
cash_available -= amount;
std::cout << "取出: " << amount << ",ATM剩余现金: " << cash_available << std::endl;
return true;
}
std::cout << "ATM现金不足,取款失败!当前剩余: " << cash_available << std::endl;
return false;
}
// 开始系统维护
void start_maintenance() {
std::lock_guard<std::mutex> guard(mtx);
maintenance_mode = true;
std::cout << "ATM进入维护模式,暂停服务" << std::endl;
}
// 结束系统维护
void end_maintenance() {
{
std::lock_guard<std::mutex> guard(mtx);
maintenance_mode = false;
std::cout << "ATM维护完成,恢复服务" << std::endl;
}
// 通知所有等待的取款线程
cv.notify_all();
}
// 补充现金
void refill_cash(double amount) {
std::lock_guard<std::mutex> guard(mtx);
cash_available += amount;
std::cout << "ATM补充现金: " << amount << ",当前总现金: " << cash_available << std::endl;
}
};
// 模拟用户线程
void user_thread(ATMSystem& atm, int user_id) {
std::cout << "用户 " << user_id << " 尝试取款..." << std::endl;
atm.withdraw(100);
}
// 模拟维护线程
void maintenance_thread(ATMSystem& atm) {
std::this_thread::sleep_for(std::chrono::milliseconds(20));
atm.start_maintenance();
// 执行维护操作
std::this_thread::sleep_for(std::chrono::milliseconds(300));
atm.refill_cash(500);
// 维护结束
atm.end_maintenance();
}
int main() {
ATMSystem atm(300); // 初始现金300元
// 启动一个维护线程和多个用户线程
std::thread maint(maintenance_thread, std::ref(atm));
std::vector<std::thread> users;
for (int i = 1; i <= 5; ++i) {
users.push_back(std::thread(user_thread, std::ref(atm), i));
}
// 等待所有线程结束
maint.join();
for (auto& t : users) {
t.join();
}
return 0;
}
总结
- mutex:最基础的锁,需要手动锁定和解锁,用不好容易出问题,就像自己管理卫生间门锁。
- lock_guard:简单安全的自动锁,构造时锁定,析构时解锁,但不能中途操作锁状态,就像请了个死板但可靠的保安。
- unique_lock:功能最全面的锁包装器,灵活性最高,但有轻微的性能开销,就像一个万能的管家。
最佳实践:
- 简单场景,优先使用
lock_guard
- 需要条件变量或灵活锁定/解锁时,使用
unique_lock
- 对性能极度敏感的场景,考虑直接使用
mutex
,但要非常小心
希望这篇文章能让你对C++11的同步工具有个清晰的认识。多线程不再可怕,熟练掌握这"三兄弟",你就能写出安全高效的并发程序啦!
加锁不迷路,C++技术一起学!
如果你觉得这篇文章对你有帮助,别忘了点赞、收藏、关注哦~ ,支持一下!你的每一次互动,都是我继续创作的动力!
有疑问?有经验想分享?欢迎在评论区和大家一起讨论,我会一一回复!
关注我的公众号「跟着小康学编程」,这里没有生涩难懂的代码讲解,只有接地气的编程知识和技巧!
在我的"公众号"里,你将获得:
- C++面试宝典:面试常考点一网打尽,HR直呼"就是你了"
- STL源码探秘:像剥洋葱一样层层解析,大神思路尽收眼底
- 性能调优秘籍:让你的代码比同事快10倍,同事直呼"666"
- 大厂项目实战:真实项目经验分享,少走弯路事半功倍
- Linux开发技巧:服务器编程从入门到精通
每周持续更新,知识干货停不下来!
记得「转发」给更多需要的朋友,让我们一起在代码世界里快乐成长!下期精彩内容,不见不散~
怎么关注我的公众号?
扫码即可关注。
哦对了,我还建了个技术交流群,大家一起聊技术、解答问题。卡壳了?不懂的地方?随时在群里提问!不只是我,群里还有一堆技术大佬随时准备帮你解惑。一起学,才有动力嘛!
「硬核科普」C++11锁机制三兄弟大比拼:mutex、lock_guard与unique_lock的更多相关文章
- 「硬核干货」总结IDEA开发的26个常用设置
前言 程序员对待IDE都是虔诚的,经常因为谁是最好的IDE而在江湖上掀起波澜,曾经我也是. 后来我遇到了IDEA,从此是它,余生都是它. IDEA 毫无疑问是目前最强大的Java开发工具了,但是大部分 ...
- 深入浅出Java并发包—锁机制(三)
接上文<深入浅出Java并发包—锁机制(二)> 由锁衍生的下一个对象是条件变量,这个对象的存在很大程度上是为了解决Object.wait/notify/notifyAll难以使用的问题. ...
- 「NGK每日快讯」12.11日NGK公链第38期官方快讯!
- 「Goravel 上新」验证表单的三种新姿势,估计你只用过一种
验证用户输入的数据是我们开发中最常见的需求,Goravel 提供三种验证姿势,个个简单好用! 第一种:简单直接式 根据表单内容直接校验: func (r *PostController) Store( ...
- 深入浅出 Java Concurrency (11): 锁机制 part 6 CyclicBarrier
如果说CountDownLatch是一次性的,那么CyclicBarrier正好可以循环使用.它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point).所谓屏障 ...
- 深入浅出 Java Concurrency (11): 锁机制 part 6 CyclicBarrier[转]
如果说CountDownLatch是一次性的,那么CyclicBarrier正好可以循环使用.它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point).所谓屏障点就 ...
- [ipsec] 特别硬核的ike/ipsec NAT穿越机制分析
〇 前言 这怕是最后一篇关于IKE,IPSEC的文字了,因为不能没完没了. 所以,我一直在想这个标题该叫什么.总的来说可以将其概括为:IKE NAT穿越机制的分析. 但是,同时它也回答了以下问题: ( ...
- 袋鼠云研发手记 | 数栈·开源:Github上400+Star的硬核分布式同步工具FlinkX
作为一家创新驱动的科技公司,袋鼠云每年研发投入达数千万,公司80%员工都是技术人员,袋鼠云产品家族包括企业级一站式数据中台PaaS数栈.交互式数据可视化大屏开发平台Easy[V]等产品也在迅速迭代.在 ...
- 全网最硬核 JVM 内存解析 - 1.从 Native Memory Tracking 说起
个人创作公约:本人声明创作的所有文章皆为自己原创,如果有参考任何文章的地方,会标注出来,如果有疏漏,欢迎大家批判.如果大家发现网上有抄袭本文章的,欢迎举报,并且积极向这个 github 仓库 提交 i ...
- [LOJ#531]「LibreOJ β Round #5」游戏
[LOJ#531]「LibreOJ β Round #5」游戏 试题描述 LCR 三分钟就解决了问题,她自信地输入了结果-- > -- 正在检查程序 -- > -- 检查通过,正在评估智商 ...
随机推荐
- Ansible - [07] 定义变量的几种方式
题记部分 Ansible 支持十几种定义变量的方式 Inventory 变量 Host Facts 变量 Register 变量 Playbook 变量 Playbook 提示变量 变量文件 命令行变 ...
- luogu-P10596题解
简要题意 一个有 \(N\) 个元素的集合有 \(2N\) 个不同子集(包含空集),现在要在这 \(2N\) 个集合中取出若干集合(至少一个),使得它们的交集的元素个数为 \(K\),求取法的方案数, ...
- 全网最简单!3分钟用满血DeepSeek R1开发一款AI智能客服,零代码轻松接入微信、公众号、小程序,新手也能快速上手!
大家好,我是狂师. 在当今数字化时代,智能客服已成为提升用户体验.提高运营效率的关键工具. 今天,我们将为大家带来一个超级简单的教程,教你如何在短短3分钟内,利用腾讯微搭平台,将满血 DeepSeek ...
- Landsat遥感影像分幅条带介绍与矢量下载:WRS的Path与Row
本文介绍Landsat系列卫星的分幅规则,并提供WRS的矢量文件下载. WRS,即Worldwide Reference System,是Landsat系列卫星全球影像标记符号系统,用以区分全 ...
- Web前端入门第 8 问:HTML <!DOCTYPE> 申明有何用处?如果没有此申明有什么问题?
HELLO,这里是大熊学习前端开发的入门笔记. 本系列笔记基于 windows 系统. 先电脑端浏览器打开任何一个网页,比如百度. 再用 ctrl + u 快捷键即可查看源码,瞅瞅第一行代码,是不是都 ...
- FastAPI性能优化指南:参数解析与惰性加载
扫描二维码关注或者微信搜一搜:编程智域 前端至全栈交流与成长 探索数千个预构建的 AI 应用,开启你的下一个伟大创意 第一章:参数解析性能原理 1.1 FastAPI请求处理管线 async def ...
- JMeter 自定义的respCode不是0就报异常
在实际使用中,后台其实已经对异常的进行了处理,response body 返回来的,都是正常的请求响应: 这个时候,则需要通过 respCode 进行判断该请求是否是有效响应. 如响应报文如下: { ...
- 解决本地代理问题 git 或者 curl Failed to connect to 127.0.0.1 port 1087 after 8 ms: Connection refused
问题出现原因 git配置了代理 本地配置了代理 执行这个命令可以看到自己的代理设置: env | grep -I proxy 临时更改代理 在当前终端执行以下命令,就可以临时将代理取消掉 export ...
- go 限流器 rate
前言 Golang 官方提供的扩展库里就自带了限流算法的实现,即 golang.org/x/time/rate.该限流器也是基于 Token Bucket(令牌桶) 实现的. 限流器的内部结构 tim ...
- 130道基础OJ编程题之: 89~107
130道基础OJ编程题之: 89~107 @ 目录 130道基础OJ编程题之: 89~107 89. BC101 班级成绩输入输出 99. BC102 矩阵元素定位 100. BC103 序列重组矩阵 ...