问题入门请想象一个场景一个寝室内有两个独立的房间但只有一个浴室如果此时的你正在洗澡但你发现你的好哥们也要使用浴室那想必一定会是尴尬的场面。这时我想你会说浴室不是有门锁着吗或者说把门锁着不久没人进得来了。恭喜你你抓住了重点锁浴室就是公共资源两个独立的房间就是独立的线程你和你的好哥们在自己房间活动是不会影响到对方的可以一旦你们要同时使用公共资源时那么你们将存在竞争关系。而有了锁就可以使得你们有次序的使用公共资源而不会出现混乱的局面。这时候有人会说了家里有两个浴室此时确实没有使用锁的必要了不过不使用锁不是本章的重点锁就是用来处理公共资源有限或同步数据修改的场景的比如两个浴室同时有三个人争用。互斥问题对公共数据如容器进行增加、删除操作时在多线程环境下需要保证操作的原子性某线程在修改过程中一气呵成不会被其他线程打断否则将会出现重大安全事故。例如线程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才能够再次重新加锁然后向下执行。恭喜你你已经会使用互斥锁了快去试试吧使用完后我们可以想想隐藏在其中的问题lock()和unlock()必须配套使用若一个线程在加锁之后提前离开并未开锁那么结果就是之后的所有线程都被挡在所外再也无法进入造成死锁现象同一线程对相同的锁进行重复加锁会导致未定义行为通常是程序直接死锁或崩溃问题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_guardstd::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_guardstd::mutex guard(mutex, std::adopt_lock); // ......其他操作 }注意使用该标志位时一定要保证目标锁已经上锁否则轻则上锁失败重则程序崩溃额外在C17中引入了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_lock比std::lock_guard灵活的另一个功能是它可以在不同作用域中转移互斥锁的归属权人话在函数中将锁返回好处是函数调用者可以在同一个锁的保护下执行其他操作std::mutex mutex; std::unique_lockstd::mutex GetLock() { std::unique_lockstd::mutex lock(mutex); //...... 其他操作 return lock; }可能会有读者疑问上述代码中返回的是局部变量使用时会不会已析构或不是同一个锁了大可放心该模板是只转移不复制的类型具体可了解std::move()相关作用同时值得注意的是std::unique_lock比std::lock_guard更灵活的代价就是因为有标志位等的存在所占内存要大一点运行效率要低一点所以请按需使用优先使用std::lock_guard同时加多个锁在标准库中提供了std::lock()用于同时给多个互斥量加锁的操作它保证了所有锁同时加锁成功否则全部加锁失败std::mutex mutex1, mutex2, mutex3, mutex4; void Func() { std::lock(mutex1, mutex2); // 配合“lock_guard”使用提前加锁 std::lock_guardstd::mutex guard(mutex1, std::adopt_lock); std::lock_guardstd::mutex guard(mutex2, std::adopt_lock); // 配合“unique_lock”使用延迟加锁 std::unique_lockstd::mutex lock1(mutex3, std::defer_lock); std::unique_lockstd::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()。若非项目设计实在需要一般不推荐使用该互斥量所以这里不做详解。注意事项对于需要加锁访问的共享数据我们不应该在函数中向外传递它的地址或引用也不要在调用的外部函数中修改它的数据不然就破坏了加锁的本意。