这本书主要讲的是c++多线程编程相关的内容,第三章是锁的盛宴
在多线程的情况下,共享数据的读写是个大问题,很容易出现a在读时b在写,导致错误,即条件竞争
而最常见的解决办法就是互斥量(mutex)
1 2 3 4 5 6 std::mutex只是互斥量,我们需要利用这个互斥量解决条件竞争,就需要用锁 mutex本身自带lock和unlock两个成员函数,可以进行上锁 但是显然这很麻烦,c++特性了属于是,很可能忘记解锁 那么RAII就登场了,std::lock_guard<std::mutex>guard(mutex)是最常见的互斥锁使用方法 但是在c++17中,引入了模板类参数推导的特性,允许在定义std::mutex之后 直接使用std::lock_guard guard(mutex)即可上锁,越出作用域之后自动释放。
在有不止一个锁的时候,正常我们是先加a锁,然后加b锁,但是因为有先后顺序,那么在多线程情况下,a锁加上后,如果b锁被另一个程序抢走,且另一个程序试图加b锁,就会出现最经典的死锁情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 std::unique_lock lock(mutex,std::defer_lock); 延迟锁本质上并没有真正的上锁,只是获取了一个指向锁的指针 template <class Mutex> unique_lock<Mutex>::unique_lock(Mutex& mtx, std::defer_lock_t) noexcept : _M_device(&mtx), _M_owns(false) { // 注意,这里没有调用 mtx.lock() // 只是记录下指针,并标记为“未加锁” } 所以当我们对锁初始化完毕后 std::lock(m1,m2);//就可以一次性对两个锁进行加锁,没有先后顺序 除了延迟锁,c++17中也引入了一个专门用于一次性加多个锁的std::scoped_lock guard() std::mutex m1, m2; std::scoped_lock lock(m1, m2); // 自动避免死锁
除了我们常见的数据的条件竞争,还有一种情况就是接口的竞争
书中举了stack的例子,比如我们使用empty()来确认stack是否为空,如果非空,我们调用pop(),但在这两步之间,因为不是原子操作,所以有可能外部会有线程修改栈,导致在栈空的情况下pop,发生未定义行为。
这是一个经典的条件竞争,使用互斥量对stack内部的数据进行保护,依然不能阻止条件竞争的发生,这是接口固有的问题。
如何解决?修改接口。比如在top或者pop时,如果栈是空,就爆出异常,但这样的话,empty()就成了无用的接口
所以实际是怎样解决的呢?c++标准库的设计师选择的是:摆烂
是的,摆烂,c++的设计哲学是:不为我用不到的东西付费
正常情况下,我们应该对栈再使用一个互斥锁,但是我们要考虑到,相当一部分的使用场景是单线程,而单线程,根本不会有这样的问题,而内部加锁毫无疑问会造成没有意义的性能损耗。那么,程序员就可以自由选择:加锁以保证安全or不加锁不(需)要安全。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 template<typename T> class threadsafe_stack { private: std::stack<T> data; mutable std::mutex m; std::condition_variable cond; public: threadsafe_stack() = default; // 禁止拷贝(互斥量不能拷贝) threadsafe_stack(const threadsafe_stack&) = delete; threadsafe_stack& operator=(const threadsafe_stack&) = delete; void push(T new_value) { std::lock_guard<std::mutex> lock(m); data.push(std::move(new_value)); cond.notify_one(); // 通知等待的消费者 } // 尝试弹出 - 非阻塞 bool try_pop(T& value) { std::lock_guard<std::mutex> lock(m); if(data.empty()) return false; value = std::move(data.top()); data.pop(); return true; } std::shared_ptr<T> try_pop() { std::lock_guard<std::mutex> lock(m); if(data.empty()) return std::shared_ptr<T>(); auto res = std::make_shared<T>(std::move(data.top())); data.pop(); return res; } // 等待弹出 - 阻塞直到有元素 void wait_and_pop(T& value) { std::unique_lock<std::mutex> lock(m); cond.wait(lock, [this]{ return !data.empty(); }); value = std::move(data.top()); data.pop(); } std::shared_ptr<T> wait_and_pop() { std::unique_lock<std::mutex> lock(m); cond.wait(lock, [this]{ return !data.empty(); }); auto res = std::make_shared<T>(std::move(data.top())); data.pop(); return res; } bool empty() const { std::lock_guard<std::mutex> lock(m); return data.empty(); } };
众所周知,std::stack的设计有些奇怪, 因为top()和pop()是两个接口,按照正常的逻辑,我们应该让top和pop合并,即pop然后返回值。
那为什么不这样做呢?毕竟分开毫无疑问会出现刚才的接口竞争问题。
1 2 3 4 5 6 7 假设 stack<vector<int>> 中 vector 元素很多,拷贝它可能因内存不足抛出 std::bad_alloc 异常。 如果 pop() 同时完成返回值和移除操作: 数据已从栈中移除。 但拷贝返回值时抛出异常 → 数据丢失。 这是假设是原子操作的情况,但就会出现以上问题,如果我们退而求其次,不要原子操作了,而是在内部实现,拷贝失败就放弃移除。可这样的话,需要内部加锁,而实际上和top,pop分开是一样的(都是非原子的)。
所以我们只能自己实现线程安全的栈(c++的魅力所在:大家一起造轮子!)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 template<typename T> class threadsafe_stack { private: std::stack<T> data; mutable std::mutex m; public: // 主要接口 - 返回智能指针 std::shared_ptr<T> pop() { std::lock_guard<std::mutex> lock(m); if(data.empty()) throw empty_stack(); auto res = std::make_shared<T>(std::move(data.top())); data.pop(); return res; } // 备选接口 - 通过参数返回 void pop(T& value) { std::lock_guard<std::mutex> lock(m); if(data.empty()) throw empty_stack(); value = std::move(data.top()); data.pop(); } // 不抛异常版本 bool try_pop(T& value) noexcept { std::lock_guard<std::mutex> lock(m); if(data.empty()) return false; value = std::move(data.top()); data.pop(); return true; } void push(T new_value) { auto new_data = std::make_shared<T>(std::move(new_value)); std::lock_guard<std::mutex> lock(m); data.push(std::move(*new_data)); } bool empty() const { std::lock_guard<std::mutex> lock(m); return data.empty(); } };
这就是以上几种解决方法:
传入一个引用,获取传出值。(大多数情况下不划算,而且数据不一定是可赋值的数据类型)
使用无异常的拷贝或者移动构造函数(无异常太麻烦,移动构造不一定都支持),实现的是try的版本,c++17以上可以用optional代替bool返回值
返回弹出值的指针(一般用shared_ptr,最理想的方法之一,坏处是对小对象造成额外的内存管理的开销,所以c++标准库不采用,而对大对象很适合)
主播在这里忽然想到了单例模式,单例模式的初始化也出现了上面empty和top/pop的问题,所以使用了双重检查,但毫无疑问这种方法有巨大的缺陷。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 1. 潜在的数据竞争问题: 问题:初始化顺序问题(Memory Reordering) 在现代多核处理器中,编译器和 CPU 可能会对指令进行优化,重新排序(reordering)执行,导致线程读取到未完全初始化的对象。这就是我们所说的 数据竞争,它是由于内存模型导致的。 具体问题在于,当 instance = new Singleton(); 被执行时,分配内存和初始化构造函数是两个独立的步骤,而 CPU 或编译器可能会重排序它们,使得其他线程能在实例完成初始化之前就读取到部分初始化的对象。 具体而言,可能的执行顺序如下: 线程 A 执行到 instance = new Singleton(); 时,首先分配了内存,但没有初始化数据,可能会修改对象指针。 线程 B 进入第一次检查,发现 instance != nullptr,因此返回该实例。 线程 B 使用的 instance 指向的可能是一个未完全初始化的对象,导致错误行为。 2. 内存模型问题: C++ 中的内存模型允许编译器和 CPU 对代码进行重排序。特别是,对于指针类型的成员(如 Singleton* instance),在没有适当的同步机制时,分配内存和初始化对象的操作可能被重排序。 例如,在 instance = new Singleton(); 语句中,内存分配和对象构造可能被重排序。这个问题尤其在多线程环境中加剧,导致不同线程看到的状态不一致。
内存重排的副作用还是有太多了(捂脸)
std::once_flag和std::call_once就是c++标准委员会那帮老头解决的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 std::call_once 与 std::once_flag 是什么 #include <mutex> std::once_flag flag; void init() { // 只会被执行一次 } void worker() { std::call_once(flag, init); // 保证 init() 只执行一次 } std::once_flag 是一个状态标志,表示某个函数是否已经被调用。 std::call_once(flag, func) 会在多线程环境中保证: func() 只会被调用一次; 其他线程在此期间会等待,直到该调用完成; 即使有异常抛出,也能确保一致性。
once_flag本质是一个标志和锁,call_once本质是用atomic和内存序实现的双重检测,解决了reordering的问题,只能说太不优雅了。
所以大佬相出了方法:Meyers’ Singleton
Meyers’ Singleton(静态局部变量) Meyers’ Singleton 是 Scott Meyers 提出的线程安全的单例模式实现方法。其核心思想是利用 C++ 中的 静态局部变量初始化 (即静态初始化在第一次调用时执行),配合 C++11 标准的线程安全机制 ,来确保单例对象只会被创建一次。
1 2 3 4 5 6 7 8 9 10 class Singleton {public : static Singleton& getInstance () { static Singleton instance; return instance; } private : Singleton () {} };
Meyers’ Singleton:
这种实现方式利用静态局部变量,C++11 保证静态局部变量的初始化是线程安全的。而且,静态局部变量 在构造时,只有第一次调用时才会创建对象,因此它也能处理异常安全问题:如果第一次创建时抛出异常,下次调用会重新执行初始化(不过通常情况下,静态局部变量不会抛出异常)。
这种开销还小,性能高,大家以后选择这种。
另外,对于非mutex的锁,如lock_guard这种,是只能加锁解锁一次的,但是unique_lock,作为唯一可以用move转移所有权的锁,在转移的过程中,本质就是解了锁又重新加
1 2 3 std::mutex mtx; std::unique_lock<std::mutex> lock_a(mtx); // 线程 A 获取锁 std::unique_lock<std::mutex> lock_b = std::move(lock_a); // 转移锁的所有权
下面介绍几种其他的锁(ai写的,懒得打了)
“读写锁”(Read–Write Lock 或 shared_mutex )是多线程同步中一个非常核心的概念, 它相比普通的互斥锁(std::mutex)更高效,尤其在 “读多写少” 的场景下。
下面我会从底层原理到实现细节,系统地讲清楚它的工作机制。
🧩 一、为什么需要读写锁? 普通的互斥锁(std::mutex)只允许:
即使是多个线程“只读”资源,也要排队,造成性能浪费。
而“读写锁”解决的就是这个问题:
操作类型
是否可并行
是否阻塞写操作
多个线程“读”
✅ 可并行
✅ 阻塞写
写线程
❌ 不可并行
✅ 阻塞读写
核心思想:
多个读者(Readers)之间可以并行;
写者(Writer)必须独占。
⚙️ 二、读写锁的三种状态 一个典型的读写锁内部可以用三个状态变量描述:
状态变量
含义
reader_count
当前正在读的线程数
writer
是否有写线程占用(布尔值)
writer_waiting_count
等待写的线程数(可选)
🧠 三、基本原理(状态机) ✅ 读锁加锁流程(lock_shared()):
获取内部锁(如自旋锁或互斥量),保护计数变量。
检查:
如果当前有写线程正在执行(writer == true),则阻塞等待。
否则增加 reader_count。
释放内部锁,允许其他读者同时进入。
🔓 读锁解锁流程(unlock_shared()):
获取内部锁。
reader_count -= 1
如果 reader_count == 0 且有写线程在等待,则唤醒写线程。
释放内部锁。
✏️ 写锁加锁流程(lock()):
获取内部锁。
检查:
如果有读线程(reader_count > 0)或写线程(writer == true),则等待。
设置 writer = true。
释放内部锁。
🔓 写锁解锁流程(unlock()):
获取内部锁。
设置 writer = false。
唤醒等待的线程:
如果有写线程在等待 → 优先唤醒写线程(防止写饥饿)。
否则唤醒所有读线程。
释放内部锁。
🧩 四、状态转换图 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ┌────────────────────────────────────────┐ │ 初始状态 │ │ reader_count = 0, writer = false │ └────────────────────────────────────────┘ │ │ lock_shared() ▼ ┌────────────────────────────────────────┐ │ 读模式(可并发) │ │ reader_count = N, writer = false │ └────────────────────────────────────────┘ │ unlock_shared() ↓ │ lock() ↑ (等待 reader_count==0) ▼ ┌────────────────────────────────────────┐ │ 写模式(独占) │ │ reader_count = 0, writer = true │ └────────────────────────────────────────┘
🧮 五、简单伪实现(C++思路) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include <mutex> #include <condition_variable> class RWLock {public : void lock_shared () { std::unique_lock<std::mutex> lk (mtx) ; read_cv.wait (lk, [this ]() { return !writer; }); ++reader_count; } void unlock_shared () { std::unique_lock<std::mutex> lk (mtx) ; if (--reader_count == 0 ) write_cv.notify_one (); } void lock () { std::unique_lock<std::mutex> lk (mtx) ; write_cv.wait (lk, [this ]() { return !writer && reader_count == 0 ; }); writer = true ; } void unlock () { std::unique_lock<std::mutex> lk (mtx) ; writer = false ; read_cv.notify_all (); write_cv.notify_one (); } private : std::mutex mtx; std::condition_variable read_cv, write_cv; int reader_count = 0 ; bool writer = false ; };
⚖️ 六、常见的公平性策略 实际实现中,读写锁有三种调度策略:
策略类型
说明
适用场景
读优先(Read-biased)
只要有读线程,就允许进入,不管是否有写线程在等
读多写少的场景(高性能)
写优先(Write-biased)
有写线程等待时,禁止新读线程进入
写多读少,避免写饥饿
公平锁(Fair RWLock)
严格按照到达顺序调度
强实时性要求
例如:pthread_rwlock 默认是 写优先 , 而 C++17 的 std::shared_mutex 是 不保证公平性 的实现(可能轻微倾向读者)。
🚀 七、C++中的标准读写锁实现 C++14:std::shared_timed_mutex C++17:std::shared_mutex(性能更高) 示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <shared_mutex> #include <thread> #include <iostream> std::shared_mutex rwlock; int shared_data = 0 ;void reader (int id) { for (int i = 0 ; i < 3 ; ++i) { std::shared_lock lock (rwlock) ; std::cout << "Reader " << id << " sees " << shared_data << "\n" ; } } void writer (int id) { for (int i = 0 ; i < 3 ; ++i) { std::unique_lock lock (rwlock) ; ++shared_data; std::cout << "Writer " << id << " increments data to " << shared_data << "\n" ; } } int main () { std::thread r1 (reader, 1 ) , r2 (reader, 2 ) , w (writer, 1 ) ; r1. join (); r2. join (); w.join (); }
🧠 八、常见问题与陷阱 1️⃣ 写者饥饿(Writer Starvation)
如果读线程源源不断进入,写线程可能长期等待。
解决:采用写优先策略(禁止新读者在有写等待时进入)。
2️⃣ 递归死锁
一个线程持有读锁后,再试图获取写锁会死锁(反之亦然)。
3️⃣ 不适合“读少写多”
在写操作频繁的情况下,锁频繁切换模式,性能可能比普通 mutex 更差。
✅ 九、总结表格
特性
互斥锁 (std::mutex)
读写锁 (std::shared_mutex)
并发读
❌ 不允许
✅ 允许多个线程同时读
写独占
✅
✅
典型场景
读写均衡
读多写少
性能
读时低效
读时高效
公平性
简单
可配置(读优先/写优先)
C++ 标准支持
C++11
C++17 起支持
🏁 十、一句话总结
读写锁的核心思想 : “多个读可以并行,一个写必须独占。”
通过计数器 + 条件变量/原子 + 内存屏障来协调这三种状态, 在“读多写少”的场景下比普通互斥锁效率更高。
递归锁
std::recursive_mutex 是 C++11 标准库中的一种互斥锁,它的主要特点是支持递归加锁 。在讨论它的使用价值之前,首先我们来理解一下递归加锁的含义和作用。
1. 递归加锁的含义 在普通的互斥锁(std::mutex)中,如果一个线程已经获得了锁,它再尝试获取该锁时将会死锁 ,因为它已经持有锁,而锁要求同一线程无法再重复获得。
但在递归互斥锁(std::recursive_mutex)中,线程可以重复获取锁,每次加锁时锁的内部计数器会递增,直到线程调用 unlock() 减少计数器,计数器归零时锁才会被完全释放。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <mutex> #include <iostream> std::recursive_mutex rmutex; void recursive_function (int count) { if (count <= 0 ) return ; rmutex.lock (); std::cout << "Lock acquired, count = " << count << std::endl; recursive_function (count - 1 ); rmutex.unlock (); std::cout << "Lock released, count = " << count << std::endl; } int main () { recursive_function (3 ); return 0 ; }
上面的代码展示了递归加锁 ,线程可以通过递归调用获取同一个 recursive_mutex 锁,而不会死锁。
2. std::recursive_mutex 的使用价值 std::recursive_mutex 的使用价值主要体现在以下几个方面:
1️⃣ 递归函数中对同一个资源的加锁 当你编写递归函数时,可能需要在每一层递归中都加锁,但又不希望因为递归的特性而导致死锁。std::recursive_mutex 就是为了解决这种情况的。例如,处理树形结构或者图遍历的递归算法时,常常会出现这种需要多次加锁的场景。
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void processTreeNode (TreeNode* node) { rmutex.lock (); if (node == nullptr ) { rmutex.unlock (); return ; } processTreeNode (node->left); processTreeNode (node->right); rmutex.unlock (); }
在这种情况下,每次递归调用都会加锁,recursive_mutex 可以确保同一个线程在递归调用时不会死锁。
2️⃣ 面向对象编程中的成员函数递归调用 在面向对象编程(OOP)中,有时会遇到需要在成员函数中递归地调用自己时,可能会再次尝试加锁。比如,如果你有一个类,它的成员函数需要在不同层级中多次访问同一共享资源,而这些成员函数又可能会递归调用自己。此时,使用 std::recursive_mutex 可以避免死锁。
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Logger {private : std::recursive_mutex mtx; public : void logMessage (const std::string& msg, int level) { mtx.lock (); if (level > 0 ) { logMessage (msg, level - 1 ); } std::cout << "Logging message: " << msg << std::endl; mtx.unlock (); } };
在上述代码中,如果 logMessage 方法在递归调用时没有使用 std::recursive_mutex,将会导致死锁,因为它会重复尝试获取同一个锁。
3️⃣ 避免不必要的锁升级 有时,当你设计的程序中需要一个函数在某些情况下递归调用自己的时候,可能会不小心增加过多的锁。比如在某些算法中,递归函数调用可能会进入深层,而在每一层加锁可能会让我们重新考虑加锁的代价。std::recursive_mutex 使得在递归调用中避免重复加锁和解锁操作。通过递归加锁,避免在每个递归调用上都增加额外的复杂性。
4️⃣ 多线程编程中的保护机制 当你设计多线程系统时,尤其是当多个线程的工作逻辑需要递归地进行时,std::recursive_mutex 提供了一种便利的机制来避免死锁和线程间的不一致性。在某些复杂的多线程应用中,recursive_mutex 是必须的工具,避免线程在递归调用过程中陷入死锁。
3. 与 std::mutex 的比较
特性
std::mutex
std::recursive_mutex
是否支持递归加锁
❌ 不支持递归加锁
✅ 支持递归加锁
加锁后是否可重复加锁
❌ 如果线程已经持有锁,重复加锁会死锁
✅ 线程可以多次加锁,直到解锁次数匹配
性能
性能较好,简单的加锁机制
性能较差,递归加锁会增加一定开销
适用场景
用于普通的互斥锁,适合一般的资源同步
用于递归调用时避免死锁(例如树/图遍历)
4. 何时应该使用 std::recursive_mutex
递归算法中需要加锁的场景 :如树形数据结构的遍历、图的深度优先遍历等,其中需要递归地对同一个共享资源进行加锁。
面向对象的成员函数递归调用 :当类的成员函数递归地调用自己,并且需要在每一层递归时持有锁时,使用 std::recursive_mutex 可以避免死锁。
避免死锁 :当你必须在递归调用中多次加锁,而不希望因为已经持有锁的线程再去加锁导致死锁时。
5. 何时不应该使用 std::recursive_mutex
避免不必要的复杂性 :如果你不需要递归加锁,只是简单的互斥操作,std::mutex 会更加高效。
性能要求较高的场景 :std::recursive_mutex 由于需要维护锁的递归计数,相比 std::mutex 会有额外的开销。因此,对于不需要递归加锁的场景,使用普通的 std::mutex 性能会更好。
6. 结论
std::recursive_mutex 的价值在于,它能够支持递归加锁,避免死锁。它适用于那些需要在递归函数或面向对象编程中多次加锁的场景。
但是,因为其维护递归计数器,性能上会比 std::mutex 略差,所以应谨慎使用。只有在真正需要递归加锁的情况下 ,才使用它。
如果你的代码中没有递归调用锁,那么使用普通的 std::mutex 会更加简单、高效。
层次锁
“层次锁(hierarchical mutex)” 是一个在多线程编程中用于防止死锁(deadlock) 的设计思想。 它并不是 C++ 标准库自带的机制,而是一种 编程约定 + 包装实现 ,用来在程序层面上强制线程按照一定的锁层次顺序获取互斥锁 。
🌳 一、为什么需要层次锁(背景) 在多线程程序中,死锁常常由锁的获取顺序不一致 造成:
1 2 3 4 5 6 7 lock (A);lock (B);lock (B);lock (A);
这两个线程各自拿到一个锁,然后等待对方释放另一个锁 → 永远卡住。
➡️ 如果能强制所有线程按照相同的顺序加锁(例如 “先锁 A 再锁 B”),就能彻底避免死锁 。
这就是层次锁(Hierarchical Lock)的基本思想。
🧩 二、层次锁的原理(核心思想) 每一个锁都被赋予一个层级编号(hierarchy value) ,通常是一个整数。
规则如下:
层级编号 越高,优先级越低 。
线程在持有一个层级为 X 的锁时,只能再去获取层级比 X 更低的锁 。
如果线程试图违反层次规则(例如先拿低层次锁再拿高层次锁),程序会抛出异常或终止,以防止潜在死锁。
这种机制可以通过封装 std::mutex 实现。
💻 三、一个简单的 C++ 实现示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #include <iostream> #include <mutex> #include <thread> #include <stdexcept> class hierarchical_mutex { std::mutex internal_mutex; const unsigned long hierarchy_value; unsigned long previous_hierarchy_value; static thread_local unsigned long this_thread_hierarchy_value; void check_for_violation () { if (this_thread_hierarchy_value <= hierarchy_value) { throw std::logic_error ("Mutex hierarchy violated" ); } } void update_hierarchy_value () { previous_hierarchy_value = this_thread_hierarchy_value; this_thread_hierarchy_value = hierarchy_value; } public : explicit hierarchical_mutex (unsigned long value) : hierarchy_value(value), previous_hierarchy_value(0 ) { } void lock () { check_for_violation (); internal_mutex.lock (); update_hierarchy_value (); } void unlock () { this_thread_hierarchy_value = previous_hierarchy_value; internal_mutex.unlock (); } bool try_lock () { check_for_violation (); if (!internal_mutex.try_lock ()) return false ; update_hierarchy_value (); return true ; } }; thread_local unsigned long hierarchical_mutex::this_thread_hierarchy_value (ULONG_MAX) ;
使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 hierarchical_mutex high_level_lock (10000 ) ;hierarchical_mutex mid_level_lock (5000 ) ;hierarchical_mutex low_level_lock (100 ) ;void low_level_func () { std::lock_guard<hierarchical_mutex> lk (low_level_lock) ; std::cout << "Low-level function\n" ; } void mid_level_func () { std::lock_guard<hierarchical_mutex> lk (mid_level_lock) ; low_level_func (); } void high_level_func () { std::lock_guard<hierarchical_mutex> lk (high_level_lock) ; mid_level_func (); } int main () { std::thread t1 (high_level_func) ; t1. join (); try { std::lock_guard<hierarchical_mutex> lk1 (low_level_lock) ; std::lock_guard<hierarchical_mutex> lk2 (high_level_lock) ; } catch (const std::logic_error& e) { std::cerr << "Caught exception: " << e.what () << '\n' ; } return 0 ; }
输出结果:
1 2 Low-level function Caught exception: Mutex hierarchy violated
✅ 正确加锁顺序(高→低)可以执行 ❌ 错误加锁顺序(低→高)直接报错,避免潜在死锁。
🧠 四、层次锁的核心价值 ✅ 优点
防止死锁
逻辑清晰
调试友好
违反层次的锁获取在运行时立刻报错,而不是死锁后难以排查。
❌ 缺点
灵活性下降
扩展困难
新增模块需要小心选择层级编号,否则可能破坏现有规则。
性能损耗
仅限“人工定义的层级”
⚙️ 五、实际使用价值 💡 在实际工业项目中: 层次锁机制 并不常见于普通业务逻辑 ,但在以下情况中非常有用:
场景
为什么有用
✅ 操作系统内核/驱动程序
通常有固定的资源层次(比如硬件 → 驱动 → 子模块),层次锁可以避免死锁。
✅ 高可靠性系统(金融、通信)
死锁的代价极高时,可以牺牲一点灵活性来换取安全性。
⚠️ 大型并发框架/数据库内核
在复杂的锁依赖中(如锁表、页锁、行锁),可借助层次锁检测逻辑错误。
❌ 一般应用程序(如 Web、GUI)
太重,维护成本高,开发者通常用 RAII、std::lock 或 std::scoped_lock 来避免死锁。
✅ 总结
特性
说明
目的
防止死锁
原理
每个锁有一个层级编号,线程只能按从高到低的顺序获取锁
检测方式
运行时检查(违反规则时报错)
优点
可彻底避免死锁;调试友好
缺点
维护复杂、限制灵活性
应用场景
高可靠性系统、内核、底层框架
普通项目建议
通常使用 std::scoped_lock 或固定加锁顺序即可
🔍 一句话总结:
层次锁是“用规则换安全”的方案: 它用严格的加锁顺序保证不会死锁,代价是灵活性和可扩展性。 在普通应用中不常用,但在高可靠性或底层系统中非常有价值。