问题入门
请想象一个场景,一个寝室内有两个独立的房间,但只有一个浴室,如果此时的你正在洗澡,但你发现你的好哥们也要使用浴室,那想必一定会是尴尬的场面。这时我想你会说浴室不是有门锁着吗,或者说把门锁着不久没人进得来了。恭喜你,你抓住了重点,锁!!!浴室就是公共资源,两个独立的房间就是独立的线程,你和你的好哥们在自己房间活动是不会影响到对方的,可以一旦你们要同时使用公共资源时,那么你们将存在竞争关系。而有了锁,就可以使得你们有次序的使用公共资源而不会出现混乱的局面。这时候有人会说了家里有两个浴室,此时确实没有使用锁的必要了,不过不使用锁不是本章的重点,锁就是用来处理公共资源有限或同步数据修改的场景的,比如两个浴室同时有三个人争用。
互斥问题
对公共数据如容器进行增加、删除操作时,在多线程环境下需要保证操作的原子性(某线程在修改过程中一气呵成,不会被其他线程打断),否则将会出现重大安全事故。例如线程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_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_lock比std::lock_guard灵活的另一个功能是,它可以在不同作用域中转移互斥锁的归属权(人话:在函数中将锁返回),好处是函数调用者可以在同一个锁的保护下执行其他操作:
std::mutex mutex; std::unique_lock<std::mutex> GetLock() { std::unique_lock<std::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_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()。若非项目设计实在需要,一般不推荐使用该互斥量,所以这里不做详解。
注意事项
对于需要加锁访问的共享数据,我们不应该在函数中向外传递它的地址或引用,也不要在调用的外部函数中修改它的数据,不然就破坏了加锁的本意。