
1. 项目概述从一道经典面试题说起最近在帮团队面试C后端开发的同学发现一个挺有意思的现象很多候选人能把互斥锁mutex、条件变量condition variable这些概念背得滚瓜烂熟甚至能说出“死锁的四个必要条件”。但当我抛出一个具体的C场景题——“如何设计代码来避免两把锁同时获取时可能引发的死锁”时能给出清晰、可落地解决方案的却不多。这道题看似基础实则像一面镜子能照出一个开发者对并发编程的理解是停留在八股文层面还是真正具备工程实战能力。死锁问题尤其是在涉及多把锁的场景下是C高性能服务开发中一个绕不开的“暗礁”。无论是实现一个线程安全的缓存、一个连接池还是一个需要同时操作多个共享数据结构的复杂业务逻辑只要涉及多个锁且获取顺序可能存在交叉死锁的风险就如影随形。网上搜索“C 死锁”你能找到海量的理论介绍和简单的双线程ABBA死锁示例。但现实中的代码往往更复杂锁的获取可能隐藏在层层函数调用之后顺序可能由运行时数据动态决定。这时仅靠死记硬背那四个必要条件互斥、请求与保持、不剥夺、循环等待是远远不够的我们需要一套系统性的工程化方法来预防和规避。这篇文章我就结合自己这些年踩过的坑和总结的经验来深度拆解这个“避免两把锁死锁”的场景。我不会只给你一个标准答案而是会带你走一遍完整的思考过程从问题本质分析到多种解决方案的对比选型再到核心代码的实现细节与避坑指南。无论你是正在准备C面试还是在实际开发中遇到了棘手的并发问题相信这些从实战中提炼出的思路和代码都能给你带来直接的帮助。2. 死锁场景深度剖析不止于“ABBA”在讨论解决方案之前我们必须先彻底理解问题。很多人对死锁的理解停留在教科书上经典的“ABBA”模型线程1先锁A再锁B线程2先锁B再锁A从而形成循环等待。这个模型没错但它过于理想化容易让人产生“我只要保证所有线程都按固定顺序如先A后B加锁就安全了”的错觉。在实际的C工程项目中死锁的产生往往更加隐蔽和复杂。2.1 现实中的复杂死锁场景场景一锁的获取隐藏在抽象背后。你封装了一个线程安全的UserManager类内部用一把锁mutex_保护用户数据。另一个OrderManager类也类似。现在有一个业务函数需要同时获取一个用户和一个订单的信息它可能这样写void ProcessUserOrder(int user_id, int order_id) { UserManager um GetUserManager(); OrderManager om GetOrderManager(); std::lock_guardstd::mutex lock1(um.GetMutex()); // 获取用户锁 std::lock_guardstd::mutex lock2(om.GetMutex()); // 获取订单锁 // ... 业务逻辑 }如果另一个地方的代码出于不同的业务逻辑先获取了订单锁再获取用户锁那么当这两个函数并发执行时死锁就发生了。问题在于锁的获取被封装在类的内部调用者可能并不清楚每个管理器内部用的具体是什么锁更难以全局统一顺序。场景二动态锁顺序。考虑一个银行转账函数Transfer(Account from, Account to, int amount)。为了保证转账原子性你需要锁住转出账户from和转入账户to。如果简单地按参数顺序先锁from再锁to那么当两个线程同时执行Transfer(A, B, 100)和Transfer(B, A, 200)时立刻就会陷入经典的ABBA死锁。锁的顺序是由运行时传入的账户对象动态决定的无法在编译期确定。场景三锁与回调/虚函数。你在持有锁的情况下调用了一个回调函数、一个虚函数、或者一段用户提供的代码比如STL算法的比较函数。这段外部代码可能在不知情的情况下又去获取了另一把锁从而间接导致了锁顺序的混乱和潜在的死锁。这种问题尤其难以调试因为死锁的调用栈可能看起来毫无关联。2.2 死锁的根源与工程化思考从工程角度看死锁问题的根源可以归结为两点锁顺序的不可控性和锁生命周期的不可见性。我们设计的解决方案本质上就是要在这两个维度上建立秩序。首先我们需要一个全局的、一致的锁排序规则。无论代码在何处、以何种方式需要获取多把锁都必须遵守同一个顺序。这就像交通规则中的“靠右行驶”必须统一否则必然混乱。其次我们需要工具和方法来强制遵守这个规则。依赖程序员的自觉和代码审查是脆弱且不可靠的尤其是在大型、多人协作的项目中。我们需要通过语言特性、库支持或设计模式让“错误的锁顺序”难以编写或者即使写了也能在早期如编译期或测试期被发现。理解了这些我们再来看具体的解决方案就不会觉得它们是一堆孤立的技巧而是一套针对不同场景、不同严格程度要求的工具箱。3. 核心解决方案对比与选型针对“避免两把锁死锁”这个问题业界有几种成熟的方案各有其适用场景和优缺点。没有一种方案是银弹选择哪种取决于你的具体需求是对性能极其敏感还是对安全性要求极高是锁的数量固定且已知还是动态变化3.1 方案一严格锁顺序Fixed Lock Ordering这是最直观、也是理论上最根本的解决方法。为系统中所有可能被同时获取的互斥锁定义一个全局的、严格的获取顺序例如通过锁的地址、ID或类型进行排序。任何需要获取多个锁的代码都必须按照这个预定义的顺序来获取。实现思路确定排序依据最常用且可靠的是使用锁对象的内存地址std::mutex*进行排序。因为每个锁的地址在程序生命周期内是唯一且不变的。在获取锁之前排序当一段代码需要获取多个锁比如锁A和锁B时先比较这两个锁的地址。按照排序结果依次加锁总是先获取地址较小或较大只要统一即可的那个锁再获取另一个。优点原理简单彻底杜绝循环等待只要所有线程都遵守同一顺序就不可能形成环。运行时开销极小仅需在加锁前进行几次指针比较没有额外的锁操作或系统调用。缺点与挑战需要全局视野和严格纪律要求所有开发者都知道并遵守这个规则。一旦某处代码违反死锁风险依然存在。可能降低并发度如果两个线程只是想操作不同的数据但因为锁顺序固定可能被迫等待同一个锁导致不必要的串行化。不适用于动态数量的锁如果需要同时获取的锁集合是运行时动态决定的比如遍历一个容器对其中每个元素加锁实现起来会非常复杂。实操心得在中小型项目或模块内部如果锁的数量不多且相对稳定采用严格锁顺序并辅以清晰的代码注释和团队约定是一个简单有效的方案。可以将排序逻辑封装成一个辅助函数如lock_ordered(mutex1, mutex2, ...)强制大家使用减少出错可能。3.2 方案二使用std::lock进行锁聚合Lock Aggregation这是C11标准库为我们提供的“官方”解决方案。std::lock函数是一个算法它可以一次性锁定两个或更多的互斥锁并且保证不会死锁。其内部通常实现了类似“尝试-回退”try-and-backoff的算法比如著名的免死锁算法Deadlock Avoidance Algorithm。工作原理简述当调用std::lock(mutex1, mutex2)时它并不是简单地先锁mutex1再锁mutex2。其内部逻辑大致如下尝试以某种顺序比如先mutex1后mutex2去获取锁。如果获取第二个锁时失败被其他线程占用它会释放已经持有的第一个锁。然后等待一小段时间可能是指数退避再换一个顺序重新尝试。如此反复直到成功同时获取所有锁。优点标准库支持安全可靠由标准库实现经过了充分测试是解决该问题的标准做法。无需关心锁的顺序调用者完全不用考虑mutex1和mutex2谁先谁后std::lock帮你处理了所有可能的竞争情况。与std::lock_guard结合异常安全可以配合std::lock和std::adopt_lock标签方便地管理锁的生命周期。缺点潜在的性能开销在竞争激烈时尝试-回退机制可能带来比固定顺序更多的上下文切换和系统调用。只能用于std::mutex等符合Lockable概念的类型对于自定义的锁或更复杂的同步原语可能不适用。3.3 方案三层次锁Hierarchical Lock这是一种通过设计来强制锁顺序的模式。它为每一把锁分配一个唯一的“层级”编号。规则是线程在持有高层级锁时不允许再获取低层级的锁。这相当于在锁之间建立了一个单向的依赖关系从根源上杜绝了循环等待的可能。实现方式定义层级常量例如constexpr int kHighLevel 1000;,constexpr int kMidLevel 500;,constexpr int kLowLevel 100;。封装一个hierarchical_mutex类它在构造时传入一个层级值。这个类内部包装了一个真正的std::mutex。hierarchical_mutex::lock()函数在加锁前会检查当前线程已持有的最高层级锁。如果要加的锁层级高于当前层级则允许如果低于或等于则抛出异常或采取其他错误处理因为这违反了层级规则。解锁时更新线程持有的层级记录。优点在运行时动态检测违规一旦代码编写错误试图以错误的顺序加锁会在运行时立即抛出异常而不是导致死锁便于调试和测试。将设计规则编码到代码中锁的顺序约束变成了类型系统的一部分更不容易被违反。缺点实现复杂需要自己封装一个智能的互斥锁类。需要精心设计层级为系统中所有锁分配合适的层级需要前期的架构设计。运行时开销每次加解锁都需要检查线程本地的层级状态。3.4 方案四一次性获取所有资源All-or-Nothing这种思路的核心是将“获取多个锁”这个操作原子化。要么一次性成功获取所有需要的锁要么一个都不获取。这通常通过一个“锁管理器”或“事务”的概念来实现。在数据库领域这类似于两阶段提交的思想。一种简单的实现是“回退法”尝试按顺序锁住第一个锁使用try_lock。如果成功继续尝试锁第二个锁。如果中途任何一个try_lock失败则释放所有已经获得的锁等待后重试或报告失败。优点逻辑清晰避免了持有部分锁等待另一把锁的状态减少了死锁窗口期。适用于复杂事务在需要锁定多个动态资源的场景下有一定优势。缺点活锁Livelock风险如果多个线程同时执行“尝试-回退”可能导致谁都成功不了一直在重试。实现复杂度高要正确处理回退和重试逻辑确保异常安全。可能降低吞吐量频繁的尝试和回退会消耗CPU资源。选型建议对于大多数通用的C应用程序方案二std::lock是首选。它是标准库的一部分提供了开箱即用的安全性和良好的性能平衡。方案一严格锁顺序在性能临界且锁关系简单的局部代码中很有效。方案三层次锁适合对代码健壮性要求极高、且愿意投入前期设计成本的大型长期项目。方案四则更多用于实现更高级别的同步抽象如软件事务内存STM的雏形。接下来我们将重点深入最实用、最标准的std::lock方案看看如何在实际代码中正确、优雅地使用它。4. 实战使用std::lock实现安全的多锁操作理论说再多不如一行代码。我们来看如何将std::lock应用到开头的ProcessUserOrder和Transfer场景中并处理各种边界情况。4.1 基础用法锁定两个mutex这是最标准的用法直接对应std::lock的两个参数版本。#include mutex std::mutex mutex1, mutex2; void safe_operation() { // 方法1使用std::lock和std::lock_guard配合std::adopt_lock std::lock(mutex1, mutex2); // 1. 一次性锁定两个锁不会死锁 std::lock_guardstd::mutex lock1(mutex1, std::adopt_lock); // 2. 接管mutex1的所有权lock_guard析构时会自动解锁 std::lock_guardstd::mutex lock2(mutex2, std::adopt_lock); // 3. 接管mutex2的所有权 // ... 临界区操作 } // lock1和lock2析构自动解锁mutex1和mutex2 void safe_operation_cpp17() { // 方法2C17起使用std::scoped_lock推荐 std::scoped_lock lock(mutex1, mutex2); // 一行代码搞定所有事情 // ... 临界区操作 } // lock析构自动解锁所有mutex核心要点std::adopt_lock是一个标签告诉lock_guard“这个mutex已经被锁住了你只需要在析构时负责解锁不要再次上锁。” 如果没有这个标签lock_guard在构造时会尝试加锁导致未定义行为很可能再次锁同一个已锁的mutex对于不可重入锁会导致死锁。std::scoped_lock是C17引入的“终极解决方案”。它相当于一个可以接收多个互斥量的lock_guard。在构造时它内部调用std::lock来一次性获取所有锁在析构时按相反顺序释放所有锁。代码更简洁、更安全是现代C的首选。4.2 解决动态锁顺序问题银行转账案例现在我们用std::scoped_lock来优雅地解决之前提到的银行转账死锁问题。class Account { private: mutable std::mutex mutex_; // mutable允许在const成员函数中加锁 double balance_; public: void transferTo(Account to, double amount) { if (this to) return; // 自我转账无需加锁 // 关键代码使用std::scoped_lock同时锁定两个账户的锁 // 锁的顺序由std::lock内部决定我们无需关心 std::scoped_lock lock(mutex_, to.mutex_); if (balance_ amount) { balance_ - amount; to.balance_ amount; std::cout Transfer successful. std::endl; } else { std::cout Insufficient funds. std::endl; } } // 锁在这里自动释放顺序与加锁顺序相反 };这段代码为什么能避免死锁当线程1执行A.transferTo(B, 100)线程2执行B.transferTo(A, 200)时线程1的scoped_lock试图锁A.mutex_和B.mutex_。线程2的scoped_lock试图锁B.mutex_和A.mutex_。std::lock算法会保证无论这两个线程以何种参数顺序调用最终只有一个线程能成功同时获取两把锁另一个线程会等待。算法内部通过“尝试-回退”机制避免了循环等待。从外部看这两个转账操作被安全地序列化了。4.3 处理两个以上锁的情况std::lock和std::scoped_lock都是可变参数模板可以处理任意数量的锁。std::mutex m1, m2, m3, m4; void complex_operation() { // 安全地一次性获取四把锁 std::scoped_lock lock_all(m1, m2, m3, m4); // 或者使用std::lockC17前 // std::lock(m1, m2, m3, m4); // std::lock_guardstd::mutex lk1(m1, std::adopt_lock); // ... // 操作受m1, m2, m3, m4保护的共享数据... }注意事项虽然可以锁任意多个但锁的数量越多持有锁的时间通常就越长对并发性能的损害就越大。在设计时应遵循“最小化锁范围”和“减少锁粒度”的原则。如果一个操作需要锁住很多资源应该重新审视设计看是否能通过拆分数据、使用更细粒度的锁或无锁数据结构来优化。4.4 与条件变量一起使用有时我们需要在持有锁的情况下等待某个条件成立这就会用到std::condition_variable。当涉及多个锁和条件变量时需要格外小心。std::mutex data_mutex; std::mutex io_mutex; std::condition_variable data_cond; bool data_ready false; std::queueint data_queue; void producer() { int data produce_data(); { // 生产数据时需要锁住数据队列 std::lock_guardstd::mutex data_lock(data_mutex); data_queue.push(data); data_ready true; } // 注意这里先释放了data_mutex // 然后再通知消费者这是一个好习惯可以减少消费者被唤醒后等待锁的时间 data_cond.notify_one(); } void consumer() { std::unique_lockstd::mutex data_lock(data_mutex); // 条件变量需要std::unique_lock // 等待条件成立。wait会在等待前释放data_lock被唤醒后重新获取。 data_cond.wait(data_lock, []{ return data_ready; }); // 此时持有data_lock安全地消费数据 int data data_queue.front(); data_queue.pop(); data_ready !data_queue.empty(); // 假设消费数据后需要进行一些日志输出需要io_mutex // 注意我们已经持有了data_lock现在需要获取io_mutex // 这里存在获取第二把锁的情况 { // 安全做法使用std::lock或scoped_lock std::scoped_lock io_lock(io_mutex, std::adopt_lock_t{}); // 但注意scoped_lock会尝试获取io_mutex而data_lock已经是unique_lock类型不同。 // 更安全的做法是先释放data_lock再按顺序加锁或者确保io操作不与其他需要data_mutex的代码形成死锁环。 // 更好的设计是将IO操作移出消费者锁的范围。 std::cout Consumed: data std::endl; } }关键陷阱在已经持有一把锁的情况下去等待一个条件变量而这个条件变量的通知可能来自一个需要获取你已持有的锁的线程这可能导致死锁。更隐蔽的是如果你在持有锁A的情况下去获取锁B而另一个线程正持有锁B并等待锁A也会死锁。当条件变量和多个锁交织时最好的建议是保持锁的简单性尽量让一个条件变量只与一把互斥锁配合使用。缩小持锁范围在调用cond.wait()之前确保只持有与之配对的那一把锁。如果需要其他资源尽量在等待结束后、或通知发出前再去获取。谨慎设计通知逻辑确保发出通知的线程不会持有会阻塞等待线程的锁。5. 进阶技巧与设计模式掌握了基础工具后我们可以通过一些设计模式和技巧从更高层面规避死锁风险。5.1 使用std::unique_lock的灵活性std::unique_lock比std::lock_guard更灵活它允许延迟加锁、手动加解锁并且可以转移所有权。这在配合std::lock时非常有用尤其是锁的数量不确定时。void lock_multiple(std::mutex m1, std::mutex m2) { // 创建两个未加锁的unique_lock std::unique_lockstd::mutex lock1(m1, std::defer_lock); std::unique_lockstd::mutex lock2(m2, std::defer_lock); // 使用std::lock一次性锁定它们管理的互斥量 std::lock(lock1, lock2); // 注意这里传入的是lock对象不是mutex // ... 临界区 } // 自动解锁std::defer_lock标签指示unique_lock在构造时不立即加锁。随后std::lock(lock1, lock2)会锁定这两个unique_lock对象关联的互斥量。这种方式清晰地将“锁对象”和“加锁动作”分离。5.2 避免在持锁时调用外部代码这是一个极其重要的原则被称为“不要在持有锁时调用未知代码”。外部代码可能尝试获取其他锁导致锁顺序问题。执行耗时操作导致锁被长期持有性能下降。抛出异常导致锁无法正常释放虽然RAII可以解决释放问题但业务状态可能不一致。不良实践std::mutex global_mutex; void bad_example() { std::lock_guardstd::mutex lock(global_mutex); // ... 一些操作 external_callback(); // 危险不知道这里面会做什么。 // ... 更多操作 }改进实践void better_example() { // 阶段1在锁保护下只进行必要的数据准备和拷贝 std::string data_to_process; { std::lock_guardstd::mutex lock(global_mutex); if (!shared_queue.empty()) { data_to_process shared_queue.front(); // 拷贝数据 shared_queue.pop(); } } // 锁在这里释放临界区尽可能短 // 阶段2在无锁状态下执行可能耗时的或调用外部代码的处理 if (!data_to_process.empty()) { external_callback(data_to_process); // 安全了 } }通过将“数据准备”和“数据处理”分离并利用栈上变量的拷贝我们大大缩小了持锁时间彻底消除了持锁调用外部代码的风险。这是一种非常有效的性能优化和死锁预防手段。5.3 使用“尝试锁”与超时机制当死锁风险无法完全通过设计避免时例如在必须与第三方库交互时可以使用“尝试锁”try_lock和超时机制作为防御性策略。std::mutex m1, m2; bool try_operation_with_timeout() { // 尝试获取第一把锁带超时 std::unique_lockstd::mutex lock1(m1, std::chrono::milliseconds(5)); if (!lock1.owns_lock()) { // 获取m1超时 log_timeout(m1); return false; // 快速失败避免长时间等待可能导致的死锁 } // 成功获取m1后尝试获取第二把锁也带超时 std::unique_lockstd::mutex lock2(m2, std::chrono::milliseconds(5)); if (!lock2.owns_lock()) { // 获取m2超时 // 注意这里lock1会在析构时自动释放m1 log_timeout(m2); return false; } // 成功获取两把锁执行操作 perform_operation(); return true; }注意try_lock和超时锁并不能“解决”死锁它们只是提供了一种“逃离”潜在死锁现场的方法避免线程无限期地阻塞。它通常用于实现乐观锁或更高级的并发控制。在死锁不可避免的复杂系统中作为最后一道防线保证系统不会完全僵死。对实时性要求高的场景不允许长时间阻塞。使用这种模式时业务逻辑需要能够处理“获取锁失败”的情况可能需要重试、回退或报告错误。6. 调试、检测与测试死锁无论多么小心复杂的并发程序仍可能引入死锁。因此掌握调试和检测死锁的工具方法至关重要。6.1 代码审查与静态分析人工审查重点关注所有lock/lock_guard/unique_lock出现的地方检查是否存在多个锁它们的获取顺序是否可能在不同执行路径下不一致。静态分析工具像Clang-Tidy这样的工具提供了诸如clang-analyzer-core.StackAddressEscape、clang-analyzer-core.NonNullParamChecker等检查器有些也能对锁的使用模式给出警告。一些商业静态分析工具对死锁的检测能力更强。6.2 动态检测工具Helgrind (Valgrind工具之一)这是一个运行时线程错误检测器可以检测数据竞争、锁顺序违规Potential deadlock等。它是发现死锁隐患的利器。valgrind --toolhelgrind ./your_cpp_programHelgrind会报告类似“Possible deadlock”的警告并给出相关的锁和调用栈信息。ThreadSanitizer (TSan)LLVM/Clang和GCC都集成了这个强大的工具。它在编译时插桩运行时检测数据竞争和死锁。# 使用Clang编译 clang -stdc17 -g -O1 -fsanitizethread -fno-omit-frame-pointer your_code.cpp -o your_program ./your_programTSan的输出非常详细能直接指出哪些线程、在哪些代码位置、因为哪些互斥量形成了死锁环。6.3 日志与断言在调试版本中可以添加丰富的日志来追踪锁的获取和释放。class InstrumentedMutex { public: void lock() { std::cout [ std::this_thread::get_id() ] Attempting to lock mutex this std::endl; mutex_.lock(); std::cout [ std::this_thread::get_id() ] Locked mutex this std::endl; owner_ std::this_thread::get_id(); } void unlock() { std::cout [ std::this_thread::get_id() ] Unlocking mutex this std::endl; owner_ std::thread::id(); mutex_.unlock(); } bool try_lock() { /* ... */ } private: std::mutex mutex_; std::atomicstd::thread::id owner_; };通过包装一个带日志的互斥量你可以清晰地看到线程间锁的争用情况帮助分析死锁发生时的现场。6.4 编写并发单元测试死锁往往在特定时序下才会触发。编写能主动制造竞争条件的单元测试非常有用。TEST(DeadlockTest, TransferDeadlock) { Account a(100), b(100); std::thread t1([]() { a.transferTo(b, 50); }); std::thread t2([]() { b.transferTo(a, 30); }); t1.join(); t2.join(); // 断言账户总额不变 EXPECT_EQ(a.getBalance() b.getBalance(), 200); }通过反复运行这样的测试可以放在循环里跑上千上万次可以增加触发潜在死锁的概率。结合TSan或Helgrind运行测试效果更佳。7. 从语言特性与设计层面根治死锁最后我们跳出“如何使用锁”的细节从更高的C语言特性和软件设计层面思考如何减少甚至消除对锁的依赖从而从根本上避免死锁。7.1 利用RAII与作用域C的RAII资源获取即初始化是管理锁的生命周期、保证异常安全的核心机制。std::lock_guard和std::unique_lock就是RAII的典范。确保每一个锁的获取都立刻与一个RAII对象绑定这能防止因异常或提前返回导致的锁泄漏这本身不会导致死锁但会导致其他线程永久等待。错误示例void risky_function() { mutex.lock(); if (some_condition) { return; // 糟糕锁没有释放 } // ... 其他操作 mutex.unlock(); // 可能因为异常也执行不到 }正确示例void safe_function() { std::lock_guardstd::mutex lock(mutex); // 构造时加锁析构时必定解锁 if (some_condition) { return; // lock析构自动解锁 } // ... 其他操作 } // lock析构自动解锁7.2 使用线程局部存储与不可变数据如果数据是只读的那么根本不需要加锁。利用const正确性和不可变数据结构immutable data structures可以消除大量同步需求。对于线程特有的数据使用thread_local关键字每个线程都有自己的副本也无需同步。// 不可变数据线程安全 class ImmutableConfig { private: const std::string server_addr_; const int port_; public: ImmutableConfig(std::string addr, int port) : server_addr_(std::move(addr)), port_(port) {} // 所有成员函数都是const的只读 std::string getAddr() const { return server_addr_; } int getPort() const { return port_; } }; // 线程局部数据无需锁 thread_local int thread_specific_counter 0; void thread_func() { thread_specific_counter; // 每个线程操作自己的副本绝对安全 }7.3 探索无锁编程与原子操作对于简单的计数器、状态标志或指针交换使用std::atomic类型可以完全避免锁。std::atomicint global_counter{0}; void increment() { global_counter.fetch_add(1, std::memory_order_relaxed); // 无锁递增 }std::atomic提供了顺序一致性(memory_order_seq_cst)或更宽松的内存序在保证正确性的同时往往能获得比互斥锁更好的性能。对于复杂的无锁数据结构如队列、栈虽然实现难度极大但在性能瓶颈关键路径上它们能带来质的提升。不过无锁编程极易出错除非万不得已且有深厚功底否则应优先考虑使用成熟的第三方无锁库。7.4 架构设计减少共享状态这是最根本的方法。重新审视你的架构是否可以通过任务队列、Actor模型、共享所有权如std::shared_ptr配合std::atomic引用计数等方式将“共享状态”转化为“消息传递”或“资源所有权传递”。任务队列生产者线程将任务放入队列消费者线程从队列取出任务执行。队列本身需要同步但任务执行过程是独立的。Actor模型每个Actor封装自己的状态只通过消息与其他Actor通信。每个Actor是单线程执行的内部无需锁。这些模式将并发控制的复杂度从数据访问层面转移到了通信层面通常更容易推理也更不容易出现死锁。避免两把锁的死锁是一个从具体语法、库函数使用到设计模式、架构思想的完整知识链。从最基础的std::lock和std::scoped_lock用起建立安全的编程习惯然后学会利用工具检测问题最后在设计和架构时时刻思考如何减少锁的竞争和持有时间。死锁并不可怕可怕的是对它的成因和解决方法没有系统的认知。希望这篇长文能成为你解决并发难题的一块坚实拼图。