news 2026/5/2 21:28:26

C++互斥

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++互斥

问题入门

请想象一个场景,一个寝室内有两个独立的房间,但只有一个浴室,如果此时的你正在洗澡,但你发现你的好哥们也要使用浴室,那想必一定会是尴尬的场面。这时我想你会说浴室不是有门锁着吗,或者说把门锁着不久没人进得来了。恭喜你,你抓住了重点,锁!!!浴室就是公共资源,两个独立的房间就是独立的线程,你和你的好哥们在自己房间活动是不会影响到对方的,可以一旦你们要同时使用公共资源时,那么你们将存在竞争关系。而有了,就可以使得你们有次序的使用公共资源而不会出现混乱的局面。这时候有人会说了家里有两个浴室,此时确实没有使用锁的必要了,不过不使用锁不是本章的重点,就是用来处理公共资源有限或同步数据修改的场景的,比如两个浴室同时有三个人争用。

互斥问题

对公共数据如容器进行增加、删除操作时,在多线程环境下需要保证操作的原子性(某线程在修改过程中一气呵成,不会被其他线程打断),否则将会出现重大安全事故。例如线程A正在对容器进写入操作,但尚未写入完成,此时若有线程B抢占该资源也进行写入,完成后退出,紧接着线程A再次获得使用权并恢复上次的修改现场,再次写入,则极有可能在容器的同一位置进行重新写入,导致覆盖线程B的修改结果。出现该问题的本质就是正在修改的公共数据的行为会被打断,也就是说,只要保证该行为的原子性就避免此种问题的发生,而互斥锁就是解决此类问题的工具之一。(标准的说法是给互斥量加锁,为了易于理解功能为主要目的,后文会直接称为锁)

互斥锁

互斥锁在头文件<mutex>中,mutex对象的三个常用函数:

  • void lock() // 加锁
  • void unlock() // 开锁
  • bool try_lock() // 尝试加锁:失败为false

使用起来也非常的简单,只需要在修改公共资源的操作之前加锁,操作完毕后解锁即可,如下:

std::mutex mtx; void Func() { mtx.lock(); // ......对公共资源进行修改 mtx.unlock(); // 切记一定要开锁 }

加锁时也可以使用try_lock()函数进行加锁并获取其返回值判断加锁是否成功。当线程A第一次进入Func()函数后执行mtx.lock();,此时线程A获得了锁的归属,它可以继续向下执行;而若此时线程B也在访问该函数,当它执行到mtx.lock();时发现mtx已经上锁,无法拿到锁的归属,就不会继续向下执行,会一直在外等待,直到线程A执行完mtx.unlock();开锁之后,线程B才能够再次重新加锁,然后向下执行。

恭喜你!你已经会使用互斥锁了,快去试试吧!使用完后我们可以想想隐藏在其中的问题:

  1. lock()unlock()必须配套使用,若一个线程在加锁之后提前离开,并未开锁,那么结果就是之后的所有线程都被挡在所外,再也无法进入,造成死锁现象;
  2. 同一线程对相同的锁进行重复加锁会导致未定义行为(通常是程序直接死锁或崩溃);
问题1:记得开锁

请思考以下代码的问题:

std::mutex mutex; void Func() { mutex.lock(); // 加锁 // 情况1:条件判断 if (ptr == nullptr) return; // 情况2:分支判断 switch (0) { case 1: // 具体操作 break; default: return; } // 情况3:抛出异常 try { // ......抛出异常的代码 } catch (const std::exception&) { return; } mutex.unlock(); // 解锁 }

以上三种判断情况都有可能导致函数提前退出,从而无法解锁,导致后面的线程永远无法继续执行,造成巨大的程序事故。
一般来说,这些分支情况需要各位大佬的小心防护,不过有好消息是,C++标准库提供了优雅的解决方式:std::lock_guard;具体用法如下:

std::mutex mutex; void Func() { std::lock_guard<std::mutex> guard(mutex); std::lock_guard guard(mutex); // 高版本C++支持自动推导 // ......操作代码 }

不必再为了开锁的位置焦头烂额,特别是那种各种分支混杂的函数。我们可以来看看std::lock_guard大概原理(只是帮助了解大概原理,这并非标准库的真正实现,请注意!!!):

class lock_guard { public: lock_guard(std::mutex &mutex) { m_mutex = mutex; m_mutex.lock(); // 加锁 } ~lock_guard() { m_mutex.unlock();// 开锁 } private: std::mutex m_mutex; };

使用lock_guard类对象进行加锁生命周期的管理,在构造函数中自动加锁,无论发生什么情况,在退出函数的那一刻,利用局部变量的自动释放,在其析构函数中进行开锁,也就保证了一定存在解锁的行为,优雅,实在是太优雅了!!!(再提醒一句,这并非lock_guard类真正的实现)。

同时,lock_guard存在第二个参数std::adopt_lock,它是一个标志位,表示当前获取的锁已经被锁住,无需再在构造函数中进行加锁,只需要保证在析构函数中解锁就行,具体用法如下:

std::mutex mutex; void Func() { mutex.lock(); // 事先锁住 // ......其他跟锁无关操作 std::lock_guard<std::mutex> guard(mutex, std::adopt_lock); // ......其他操作 }

注意:使用该标志位时一定要保证目标锁已经上锁,否则轻则上锁失败,重则程序崩溃!

额外,在C++17中,引入了std::scoped_lock,它是std::lock_guard增强版。

更灵活的加锁

在标准库中,std::unique_lock也可以用于加锁管理,基本用法与std::lock_guard一致,灵活在于它是一个模板类型,也支持第二参数,含义如下:

  • std::adopt_lock:同上;
  • std::defer_lock:与std::adopt_lock刚好相反,它表示目标锁在初始化时不上锁,但在后续的使用中需要手动调用lock()函数进行加锁。(std::unique_lock模板类存在lock()unlock()try_lock()等函数)
  • std::try_to_lock:在构造函数中尝试加锁,但不阻塞。

std::unique_lockstd::lock_guard灵活的另一个功能是,它可以在不同作用域中转移互斥锁的归属权(人话:在函数中将锁返回),好处是函数调用者可以在同一个锁的保护下执行其他操作:

std::mutex mutex; std::unique_lock<std::mutex> GetLock() { std::unique_lock<std::mutex> lock(mutex); //...... 其他操作 return lock; }

可能会有读者疑问,上述代码中返回的是局部变量,使用时会不会已析构或不是同一个锁了,大可放心,该模板是只转移不复制的类型,具体可了解std::move()相关作用;

同时,值得注意的是,std::unique_lockstd::lock_guard更灵活的代价就是因为有标志位等的存在,所占内存要大一点,运行效率要低一点,所以请按需使用,优先使用std::lock_guard

同时加多个锁

在标准库中提供了std::lock()用于同时给多个互斥量加锁的操作,它保证了所有锁同时加锁成功,否则全部加锁失败;

std::mutex mutex1, mutex2, mutex3, mutex4; void Func() { std::lock(mutex1, mutex2); // 配合“lock_guard”使用提前加锁 std::lock_guard<std::mutex> guard(mutex1, std::adopt_lock); std::lock_guard<std::mutex> guard(mutex2, std::adopt_lock); // 配合“unique_lock”使用延迟加锁 std::unique_lock<std::mutex> lock1(mutex3, std::defer_lock); std::unique_lock<std::mutex> lock2(mutex4, std::defer_lock); std::lock(mutex3, mutex4); }
问题2:重复加锁

为了解决同一线程对同一互斥锁进行多次加锁导致未定义的行为,标准库提供了递归锁std::recursive_mutex类型的mutex,其工作方式与std::mutex相似,但值得注意的是,对std::recursive_mutex类型的互斥量加多少次锁lock(),就必须调用相应次数的开锁unlock()。若非项目设计实在需要,一般不推荐使用该互斥量,所以这里不做详解。

注意事项

对于需要加锁访问的共享数据,我们不应该在函数中向外传递它的地址或引用,也不要在调用的外部函数中修改它的数据,不然就破坏了加锁的本意。
,对std::recursive_mutex类型的互斥量加多少次锁lock(),就必须调用相应次数的开锁unlock()。若非项目设计实在需要,一般不推荐使用该互斥量,所以这里不做详解。

注意事项

对于需要加锁访问的共享数据,我们不应该在函数中向外传递它的地址或引用,也不要在调用的外部函数中修改它的数据,不然就破坏了加锁的本意。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/2 21:22:25

Taotoken在多模型聚合调用中表现出的路由稳定性体验

Taotoken在多模型聚合调用中表现出的路由稳定性体验 1. 多模型聚合调用的核心需求 在实际开发场景中&#xff0c;接入多个大模型供应商已成为常见需求。开发者通常需要根据业务特点选择不同供应商的模型&#xff0c;同时确保服务的高可用性。Taotoken作为大模型聚合分发平台&…

作者头像 李华
网站建设 2026/5/2 21:22:25

SignatureTools安卓APK签名工具终极指南:3分钟完成专业签名

SignatureTools安卓APK签名工具终极指南&#xff1a;3分钟完成专业签名 【免费下载链接】SignatureTools &#x1f3a1;使用JavaFx编写的安卓Apk签名&渠道写入工具&#xff0c;方便快速进行v1&v2签名。 项目地址: https://gitcode.com/gh_mirrors/si/SignatureTools …

作者头像 李华
网站建设 2026/5/2 21:16:26

STM32F407VET6 CAN通信实战:从CubeMX配置到收发调试(附完整代码)

STM32F407VET6 CAN通信实战&#xff1a;从CubeMX配置到收发调试&#xff08;附完整代码&#xff09; CAN总线作为工业控制领域的核心通信协议&#xff0c;其稳定性和实时性直接影响电机控制等关键系统的性能。本文将基于STM32F407VET6芯片&#xff0c;通过CubeMX工具链完成从硬…

作者头像 李华