C++11锁机制mutex、lock_guard与unique_lock详细解析
目录
- 为啥要用这些同步工具?
- 老大:mutex(互斥锁)
- 老二:lock_guard(保安大哥)
- 老三:unique_lock(万能管家)
- 三兄弟大比拼
- 实战:模拟ATM取款与系统维护
- 总结
大家好啊,我是小康。今天咱们聊点"家常"——那些让C++程序员又爱又恨的多线程同步工具!
如果你曾经被多线程搞得头大,或者听到"死锁"就心慌,那这篇文章就是为你准备的。今天我要用最接地气的方式,帮你彻底搞懂C++11中的三兄弟:mutex
、lock_guard
和unique_lock
。
为啥要用这些同步工具?
先别急着学怎么用,咱们得先知道为啥要用啊!
想象一下:你和室友共用一个卫生间。如果你们同时冲进去...嗯,画面太美不敢想象。所以你们会怎么做?肯定是先看看有没有人,没人才进去,然后反锁门,用完了再开门。
多线程程序也一样!不同的线程可能会同时访问同一块"地盘"(共享资源),如果不加控制,就会出现数据错乱、程序崩溃等一系列灾难。
这时候,我们的三兄弟就闪亮登场了!
老大:mutex(python互斥锁)
mutex
就像那个卫生间的门锁,它是最基础的同步工具,核心功能就两个:锁上(lock
)和开锁(unlock
)。
来看个最简单的例子:
#include <IOStream> #include <thread> #include <mutex> std::mutex mtx; // 这就是我们的"门锁" int shared_value = 0; // 这是我们要保护的"卫生间" void increment_valu编程e() { 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:www.devze.com: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 <threandroidad> #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++11锁机制三兄弟大比拼:mutex、lock_guard与unique_lock的文章就介绍到这了,更多相关C++ mutex、lock_guard与unique_lock内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!
精彩评论