1. 项目概述:为什么我们需要读写锁?
在C++多线程编程里,锁是保护共享数据、避免数据竞争的核心工具。最基础的互斥锁(std::mutex)简单粗暴,谁拿到锁谁就能访问数据,其他线程都得等着。这在很多场景下没问题,但遇到一种典型情况就显得效率低下了:读多写少。
想象一个配置管理模块,一个全局的配置对象,后台线程可能每隔几分钟才更新一次(写操作),而前台可能有上百个业务线程每秒都在读取配置(读操作)。如果用普通的互斥锁,即使所有线程都只是想“看一眼”数据,也必须要排队,一个读完了下一个才能读。这造成了不必要的性能瓶颈,因为多个线程同时读取数据,只要数据不被修改,是绝对安全的,完全不需要互斥。
这就是读写锁(Read-Write Lock,或称共享-独占锁)要解决的问题。它的核心思想是将锁的访问模式区分开:
- 共享(读)锁:允许多个线程同时获取,用于只读操作。只要没有线程持有写锁,读锁就可以被多个线程同时持有。
- 独占(写)锁:一次只允许一个线程获取,用于写入操作。当一个线程持有写锁时,其他任何线程(无论是想读还是想写)都无法获取锁。
这种设计在“读多写少”的场景下能极大提升程序的并发吞吐量。我经历过一个日志分析服务,最初用互斥锁保护一个内存中的统计数据映射表,在高并发查询时CPU大量消耗在锁等待上。后来换用读写锁,查询性能直接提升了七八倍,效果立竿见影。
C++标准库在C++14之前并没有提供原生的读写锁,直到C++17才在标准中加入了std::shared_mutex和std::shared_timed_mutex。但在那之前,以及在一些需要特殊定制策略(比如写者优先、公平锁)的场景下,我们仍然需要了解如何自己动手实现一个读写锁。这不仅有助于理解其底层原理,也是应对复杂并发场景、进行深度性能调优的必备技能。
2. 读写锁的核心设计思路与权衡
实现一个可用的读写锁,远不止“读可以共享,写必须独占”这么简单。背后有一系列需要仔细权衡的设计决策,直接影响到锁的公平性、吞吐量和复杂度。
2.1 读者与写者的优先级问题
这是设计读写锁时第一个要面对的抉择:当读锁和写锁同时竞争时,谁该优先获得资源?
读者优先(Read-preferring):这是最常见、也最容易实现的策略。只要有一个读者持有了读锁,后续到达的读者可以无视正在等待的写者,直接获取读锁。这能最大化读的并发度,但可能导致“写者饥饿”(Writer Starvation)。想象一下,如果读请求源源不断,那个可怜的写者可能永远都等不到所有读者释放锁的那一刻。
- 优点:读性能极高,实现简单。
- 缺点:写操作可能被无限期延迟,不适合写操作有实时性要求的场景。
写者优先(Write-preferring):一旦有写者在等待,新到来的读者必须排队,直到所有等待的写者完成。这保证了写操作的及时性,避免了写者饥饿。
- 优点:写操作的延迟可预测,数据更新更及时。
- 缺点:在读多写少的场景下,会损害读的并发性能,因为读者经常需要给写者让路。
公平策略(Fair):通常按照FIFO(先进先出)的顺序来授予锁,无论是读者还是写者。这避免了饥饿,但通常需要更复杂的队列管理,并且可能降低整体吞吐量,因为它破坏了读者可以完全并发的特性。
实操心得:在绝大多数业务场景中,“读者优先”的策略是默认且合理的选择,因为它契合了“读多写少”的假设,能带来最大的收益。只有当写操作的延迟变得不可接受(例如,一个实时控制系统需要立刻更新状态),或者你监测到明显的写者饥饿现象时,才需要考虑更复杂的公平或写者优先策略。C++标准库的
std::shared_mutex通常实现为读者优先。
2.2 状态管理与同步原语选择
一个读写锁内部需要维护几个关键状态:
- 读者计数(reader_count):当前有多少个线程持有读锁。
- 写者标记(writer_active):是否有线程正在写(或正在尝试写)。
- 等待的写者计数(writer_waiting):有多少个写线程在排队。
我们需要用原子操作或互斥锁来保护这些状态的读写,确保其一致性。同时,还需要让等待的线程能够休眠(而不是忙等待),这就需要条件变量(std::condition_variable)来帮忙。
基础工具选择:
- 互斥锁(
std::mutex):用于保护内部状态变量的“元锁”。 - 条件变量(
std::condition_variable):用于让等待读或写的线程挂起和唤醒。 - 原子变量(
std::atomic):可以用于优化,比如读者计数,在某些实现中可以用原子操作来递增递减,减少对“元锁”的争用。
一个经典的“读者优先”读写锁实现框架如下:
- 内部有一个
std::mutex(称为mtx_)保护所有状态。 - 两个
std::condition_variable:reader_cv_(读者等)和writer_cv_(写者等)。 - 状态变量:
active_readers_(当前活跃读者数),active_writer_(是否有活跃写者),waiting_writers_(等待的写者数)。
2.3 递归锁与锁升级/降级
- 递归锁:同一个线程能否多次获取同一个读锁或写锁?标准库的
std::mutex默认是非递归的,重复加锁会导致死锁。std::shared_mutex通常也不支持递归。自己实现递归支持会大大增加复杂度,一般不建议,除非有非常特殊的业务需求。 - 锁升级/降级:这是一个高级特性。锁升级是指一个持有读锁的线程,能否在不释放读锁的情况下,将其“升级”为写锁?锁降级则相反。这非常有用(例如,先读数据判断是否需要修改,需要则升级),但极易导致死锁。例如,两个线程都持有读锁并尝试升级,就会互相等待对方释放读锁,形成死锁。因此,大多数简单实现(包括标准库)都不直接支持。如果需要,必须在应用层通过非常谨慎的协议来处理。
注意事项:在你自己的第一个读写锁实现中,不要考虑递归和升级/降级。先实现一个基础、正确、非递归的版本。99%的场景这已经足够。过早引入这些高级特性会让代码复杂度呈指数级增长,并且极易引入难以发现的并发Bug。
3. 动手实现一个“读者优先”的读写锁
理论说再多,不如动手写一遍。下面我们基于C++11的标准库,实现一个非递归、读者优先的读写锁类ReadWriteLock。我们会逐步拆解,并解释每一行代码的意图。
3.1 类定义与成员变量
#include <mutex> #include <condition_variable> class ReadWriteLock { public: ReadWriteLock() = default; ~ReadWriteLock() = default; // 禁止拷贝和赋值 ReadWriteLock(const ReadWriteLock&) = delete; ReadWriteLock& operator=(const ReadWriteLock&) = delete; void lock_read(); // 获取读锁 void unlock_read(); // 释放读锁 void lock_write(); // 获取写锁 void unlock_write(); // 释放写锁 private: mutable std::mutex mtx_; // 保护内部状态的核心互斥锁 std::condition_variable reader_cv_; // 读者等待的条件变量 std::condition_variable writer_cv_; // 写者等待的条件变量 int active_readers_ = 0; // 当前持有读锁的线程数 bool active_writer_ = false; // 是否有线程持有写锁 int waiting_writers_ = 0; // 正在等待写锁的线程数 };成员变量解析:
mtx_:这是整个锁的“大门锁”,任何对active_readers_、active_writer_、waiting_writers_的修改都必须先锁住它。它被标记为mutable,是因为在const成员函数(如果存在)中也可能需要修改状态。reader_cv_&writer_cv_:当线程无法立即获得锁时,就在对应的条件变量上等待。当锁的状态发生变化(如写锁释放、读锁全部释放)时,需要通知(notify_one或notify_all)这些等待的线程。active_readers_:核心状态。大于0表示有读者在活动。active_writer_:为true时,表示写者正在活动或有写者刚拿到锁准备活动。此时不允许任何其他读写。waiting_writers_:用于实现“写者优先”或做统计。在我们读者优先的实现中,它的主要作用是让新来的读者知道有写者在等,但根据读者优先策略,新读者依然可以插队。
3.2 读锁的实现:lock_read与unlock_read
void ReadWriteLock::lock_read() { std::unique_lock<std::mutex> lock(mtx_); // 等待条件:不能有活跃的写者,并且最好也没有等待的写者(严格读者优先可以忽略等待写者) // 这里我们实现一个“弱读者优先”:只要没有活跃写者,读者就可以上锁,不管有没有写者在等。 reader_cv_.wait(lock, [this]() { return !active_writer_; // 只要没有线程正在写或持有写锁,读者就可以进入 }); ++active_readers_; // 读锁获取成功,unique_lock 析构时会自动释放 mtx_ } void ReadWriteLock::unlock_read() { std::unique_lock<std::mutex> lock(mtx_); --active_readers_; // 检查是否所有读者都释放了锁 if (active_readers_ == 0) { // 如果此时有写者在等待,唤醒一个写者 // 注意:这里唤醒一个,而不是所有。因为一次只能有一个写者活动。 writer_cv_.notify_one(); } // 如果 active_readers_ > 0,说明还有其他读者,什么都不用做,让它们继续读。 }lock_read关键点:
- 首先获取保护内部状态的互斥锁
mtx_。 - 使用
reader_cv_.wait在条件上等待。这里的条件是!active_writer_。这意味着,只要当前没有写者活跃(即active_writer_ == false),线程就会跳出等待,继续执行。即使waiting_writers_ > 0(有写者在排队),新来的读者依然可以获取读锁,这就是“读者优先”的体现。 - 条件满足后,增加
active_readers_计数,函数返回,读锁获取成功。lock(unique_lock)在函数退出时析构,自动释放mtx_。
unlock_read关键点:
- 同样先锁住
mtx_。 - 减少读者计数。
- 最重要的逻辑:如果读者计数减到0(
active_readers_ == 0),说明这是最后一个释放读锁的线程。此时,可能正有写者在苦苦等待。所以我们需要调用writer_cv_.notify_one()来唤醒一个等待的写者。如果读者计数不为零,则什么都不用做,其他读者还在活动。
3.3 写锁的实现:lock_write与unlock_write
void ReadWriteLock::lock_write() { std::unique_lock<std::mutex> lock(mtx_); ++waiting_writers_; // 标记有一个写者在等待 // 等待条件:没有任何活跃的读者,也没有任何活跃的写者 writer_cv_.wait(lock, [this]() { return (active_readers_ == 0) && !active_writer_; }); --waiting_writers_; // 成功获得锁,离开等待队列 active_writer_ = true; // 标记写者活跃 // 写锁获取成功 } void ReadWriteLock::unlock_write() { std::unique_lock<std::mutex> lock(mtx_); active_writer_ = false; // 标记写者不再活跃 // 解锁后,该唤醒谁? // 策略1:优先唤醒等待的写者(写者优先)。但这里我们实现读者优先。 // 策略2:优先唤醒等待的读者(读者优先)。 // 一个常见的折中策略:如果有写者在等,先唤醒写者;否则,唤醒所有读者。 if (waiting_writers_ > 0) { writer_cv_.notify_one(); // 唤醒一个写者 } else { reader_cv_.notify_all(); // 唤醒所有读者 } }lock_write关键点:
- 先锁住
mtx_,然后立即将waiting_writers_加1。这个计数很重要,它让unlock_write和lock_read能知道当前是否有写者在排队。 - 在条件变量
writer_cv_上等待。等待的条件非常严格:必须同时满足active_readers_ == 0(没有读者)和!active_writer_(没有其他写者)。这保证了写锁的独占性。 - 当条件满足(即之前的读写锁全部释放),线程被唤醒,首先将
waiting_writers_减1(因为自己马上就要获得锁,离开等待队列),然后将active_writer_设为true,标识写者开始活动。
unlock_write关键点:
- 将
active_writer_设为false,表示写操作结束。 - 唤醒策略是核心。这里采用了一个兼顾公平和性能的策略:
- 首先检查
waiting_writers_。如果有写者在排队,调用writer_cv_.notify_one()唤醒其中一个。这避免了写者被读者“饿死”,因为写者释放锁后,如果后面还有写者在等,会优先让下一个写者上。这可以看作是一种“局部的写者优先”。 - 如果没有写者在等(
waiting_writers_ == 0),则调用reader_cv_.notify_all()唤醒所有正在等待的读者。注意这里是notify_all,因为读锁是共享的,可以同时唤醒多个读者,让它们一起获取锁,最大化并发度。
- 首先检查
实操心得:这个唤醒策略(先检查等待的写者)是对“纯读者优先”的一个微小但重要的改进。纯读者优先在
unlock_write时可能直接reader_cv_.notify_all(),这会导致如果读请求不断,写者可能永远无法获得锁。先检查并唤醒等待的写者,给了写者一个机会,显著缓解了写者饥饿问题,在实践中是一个更健壮的选择。
3.4 使用示例与RAII包装器
直接使用lock_read/unlock_read这种原始接口很容易出错,比如忘记解锁。我们应该遵循RAII(资源获取即初始化)原则,像标准库的std::lock_guard和std::unique_lock一样,用对象生命周期来管理锁。
// 读锁的RAII包装器 class ReadLockGuard { public: explicit ReadLockGuard(ReadWriteLock& rw_lock) : rw_lock_(rw_lock) { rw_lock_.lock_read(); } ~ReadLockGuard() { rw_lock_.unlock_read(); } // 禁止拷贝 ReadLockGuard(const ReadLockGuard&) = delete; ReadLockGuard& operator=(const ReadLockGuard&) = delete; private: ReadWriteLock& rw_lock_; }; // 写锁的RAII包装器 class WriteLockGuard { public: explicit WriteLockGuard(ReadWriteLock& rw_lock) : rw_lock_(rw_lock) { rw_lock_.lock_write(); } ~WriteLockGuard() { rw_lock_.unlock_write(); } WriteLockGuard(const WriteLockGuard&) = delete; WriteLockGuard& operator=(const WriteLockGuard&) = delete; private: ReadWriteLock& rw_lock_; }; // 使用示例 #include <iostream> #include <vector> #include <thread> ReadWriteLock rw_lock; std::vector<int> shared_data = {1, 2, 3}; void reader(int id) { ReadLockGuard lock(rw_lock); // 构造时加读锁,析构时自动释放 // 安全的读操作 std::cout << "Reader " << id << " sees: "; for (int num : shared_data) { std::cout << num << ' '; } std::cout << std::endl; // lock 对象在此处析构,自动调用 unlock_read } void writer(int id, int new_value) { WriteLockGuard lock(rw_lock); // 构造时加写锁,析构时自动释放 // 安全的写操作 std::cout << "Writer " << id << " is writing " << new_value << std::endl; shared_data.push_back(new_value); // 模拟写操作耗时 std::this_thread::sleep_for(std::chrono::milliseconds(100)); // lock 对象在此处析构,自动调用 unlock_write } int main() { std::vector<std::thread> threads; // 启动多个读者 for (int i = 0; i < 5; ++i) { threads.emplace_back(reader, i); } // 启动一个写者 threads.emplace_back(writer, 0, 99); // 再启动多个读者 for (int i = 5; i < 10; ++i) { threads.emplace_back(reader, i); } for (auto& t : threads) { t.join(); } return 0; }使用RAII包装器后,代码安全性和简洁性得到了极大提升。即使函数中间有return语句或抛出异常,锁也能被正确释放,避免了资源泄漏和死锁。
4. 进阶话题:性能优化与标准库对比
我们上面实现的是一个正确但相对基础的版本。在生产环境中,我们还需要考虑性能。
4.1 性能瓶颈分析与优化方向
我们实现的锁性能瓶颈主要在mtx_这个“元锁”上。每一次lock_read和lock_write调用,无论成功与否,都需要先获取这个互斥锁。在高并发、锁竞争激烈的场景下,这个mtx_本身就会成为热点。
优化思路是减少对中心互斥锁的依赖。一个经典的优化是使用“原子操作+回退”的策略。
优化版思路(以读锁为例):
- 读者尝试获取锁时,先使用一个原子的读者计数(例如
std::atomic<int> active_readers)进行fetch_add操作。 - 操作前和操作后,可以通过检查一个“写者意图”或“写者已持有”的原子标志来判断在操作过程中是否有写者介入。
- 如果检测到冲突,则回退到使用传统的互斥锁+条件变量的慢路径。
这种思路类似于“乐观锁”:先假设没有冲突进行操作,发生冲突时再回退。std::shared_mutex在主流标准库(如GCC的libstdc++, LLVM的libc++)中的实现,就大量使用了这种原子操作和自旋等待的优化,性能远高于我们上面基于纯互斥锁和条件变量的朴素实现。
4.2 与C++17std::shared_mutex的对比
从C++17开始,我们应该优先使用标准库提供的std::shared_mutex(或带超时功能的std::shared_timed_mutex)。
#include <shared_mutex> std::shared_mutex s_mutex; // 读锁 { std::shared_lock<std::shared_mutex> lock(s_mutex); // RAII读锁 // ... 读操作 } // 锁自动释放 // 写锁 { std::unique_lock<std::shared_mutex> lock(s_mutex); // RAII写锁 // ... 写操作 } // 锁自动释放为什么用标准库?
- 正确性:标准库的实现经过千锤百炼,由顶尖的并发专家编写和审核,几乎不存在边界条件的Bug。
- 性能:如前所述,标准库的实现采用了平台相关的最优优化(如使用futex on Linux, SRW locks on Windows),性能通常比自己实现的通用版本要好。
- 可移植性:代码可以在任何支持C++17的平台上运行。
- 功能丰富:
std::shared_timed_mutex还提供了try_lock_for,try_lock_until等带超时的接口。
什么时候需要自己实现?
- 兼容旧编译器:项目受限于C++11或C++14标准。
- 需要特殊策略:标准库的
shared_mutex通常实现为读者优先。如果你需要严格的写者优先、公平锁,或者特定的优先级策略,可能需要自己定制。 - 教学与研究:为了深入理解并发原语的工作原理。
- 极致性能与定制:在某些极端性能敏感的场景,你可能需要对锁的行为有完全的控制(例如,禁用某些特性以减少内存占用或指令数)。
注意事项:在99%的应用场景中,直接使用
std::shared_mutex是最佳选择。自己实现读写锁更多是出于学习目的,或者是在一个更大的、自定义的同步原语框架中的一部分。切勿在生产环境中为了“炫技”而重新发明一个轮子,除非你能证明标准库的实现在你的特定负载下确实是瓶颈,并且你的实现能稳定地带来可观的性能提升。
5. 常见陷阱、调试与测试技巧
即使理解了原理,并发编程依然处处是坑。下面分享一些在实现和使用读写锁时容易遇到的问题和应对方法。
5.1 典型陷阱与死锁场景
锁的顺序不一致(Lock Ordering):这是死锁最常见的原因。如果多个线程需要同时获取锁A和锁B,但线程1按顺序A->B获取,线程2按顺序B->A获取,就可能发生死锁。
- 应对:建立全局的锁获取顺序。如果代码中同时使用了读写锁和普通互斥锁,规定一个固定的获取顺序(例如,总是先获取互斥锁M,再获取读写锁R)。
在持有锁时调用未知代码:这是一个容易被忽视的坑。例如,在持有写锁时,调用了一个回调函数或虚函数,而这个函数内部又试图去获取同一个锁(递归,但锁不支持),或者获取其他锁,很容易导致死锁或难以理解的行为。
- 应对:最小化临界区。持有锁的时间应尽可能短,只做必要的共享数据操作。绝对不要在锁内进行I/O操作、网络请求或调用可能重入的用户代码。
误用RAII包装器:例如,错误地使用了
std::lock_guard<std::shared_mutex>(这是写锁)来保护读操作,虽然功能正确,但完全丧失了读并发的能力,退化成了互斥锁。- 应对:清晰地区分
std::shared_lock(读)和std::unique_lock/std::lock_guard(写)。给变量起有意义的名字,如read_lock,write_lock。
- 应对:清晰地区分
“读者优先”实现中的写者饥饿:就像我们之前分析的,如果读请求持续不断,写者可能永远无法获得锁。我们的实现通过
unlock_write时优先唤醒写者来缓解,但并非根治。- 监控与调优:在关键锁上添加统计信息(如等待时间、获取次数)。如果发现写者平均等待时间过长,就需要考虑调整策略,例如在
lock_read的等待条件中加入对waiting_writers_的判断,当等待的写者超过一定阈值时,让新读者也排队。
- 监控与调优:在关键锁上添加统计信息(如等待时间、获取次数)。如果发现写者平均等待时间过长,就需要考虑调整策略,例如在
5.2 调试与排查并发Bug的工具
并发Bug难以复现,需要借助工具。
- 代码审查:多人仔细检查锁的获取和释放是否成对出现,顺序是否一致。
- ThreadSanitizer (TSan):这是Clang/GCC编译器提供的动态分析工具,能检测数据竞争、死锁等。在编译时添加
-fsanitize=thread标志,运行时就能获得详细的竞争报告。 - Helgrind 和 DRD:Valgrind工具套件中的线程错误检测工具,功能类似TSan,不需要重新编译但运行较慢。
- 日志与断言:在锁的实现中加入大量的调试日志(通过宏控制,仅在调试版本开启),记录锁的获取、释放、等待、唤醒等事件。使用断言检查不变量,例如“释放写锁时,
active_writer_必须为true”。
5.3 如何测试你的读写锁实现
测试并发数据结构非常挑战。不能只测功能正确性,还要测并发安全性和性能。
功能正确性测试(单线程):
- 测试锁的基本获取/释放。
- 测试读锁可重入吗?(我们的实现不支持,应确保重复调用
lock_read会死锁或报错)。 - 测试读锁和写锁的互斥关系。
并发安全性与压力测试(多线程):
- 读者-写者模型:启动大量读者线程和少量写者线程,操作一个共享计数器。写者递增计数器,读者读取计数器。最后,写者的总递增次数必须等于计数器的最终值减去初始值。同时,读者读到的值应该是一个合法的中间状态(例如,不能读到部分更新的数据,虽然对于简单
int原子操作可能没问题,但这里测试的是锁的互斥性)。 - 使用原子操作验证:共享数据可以是一个结构体,写者同时更新多个字段,读者验证读到的多个字段是否来自同一个“版本”(即写锁保护下的一个一致状态)。
- 长时间压力测试:让测试程序运行数小时甚至数天,观察是否有死锁或数据损坏。
- 读者-写者模型:启动大量读者线程和少量写者线程,操作一个共享计数器。写者递增计数器,读者读取计数器。最后,写者的总递增次数必须等于计数器的最终值减去初始值。同时,读者读到的值应该是一个合法的中间状态(例如,不能读到部分更新的数据,虽然对于简单
性能基准测试:
- 对比你的实现与
std::shared_mutex在不同读者/写者比例、不同线程数量下的吞吐量(每秒完成的操作数)和延迟。 - 工具可以使用Google Benchmark。
- 对比你的实现与
一个简单的并发安全测试思路:
#include <atomic> #include <vector> #include <thread> #include <cassert> void test_concurrent_rw(ReadWriteLock& lock) { const int kNumIterations = 100000; std::atomic<int> shared_value{0}; std::atomic<int> read_checksum{0}; // 读者读到的值的总和 std::atomic<int> write_increments{0}; // 写者增加的次数总和 auto reader = [&]() { int local_sum = 0; for (int i = 0; i < kNumIterations; ++i) { ReadLockGuard guard(lock); local_sum += shared_value.load(std::memory_order_relaxed); } read_checksum.fetch_add(local_sum, std::memory_order_relaxed); }; auto writer = [&]() { for (int i = 0; i < kNumIterations; ++i) { WriteLockGuard guard(lock); int old_val = shared_value.fetch_add(1, std::memory_order_relaxed); // 旧值应该是我们增加之前的,可以用于更复杂的断言 (void)old_val; write_increments.fetch_add(1, std::memory_order_relaxed); } }; std::vector<std::thread> threads; // 启动多个读者和写者 for (int i = 0; i < 4; ++i) threads.emplace_back(reader); for (int i = 0; i < 2; ++i) threads.emplace_back(writer); for (int i = 0; i < 4; ++i) threads.emplace_back(reader); // 再混合一些读者 for (auto& t : threads) t.join(); // 验证:最终值 = 写者增加的总次数 assert(shared_value == write_increments); // 更严格的验证:读者读到的总和应该在一个合理的范围内。 // 由于并发,读者可能读到任何中间值,但总和应该大致等于 (最终值 + 初始值) / 2 * 读取次数。 // 这里主要是为了触发并发访问,死锁或数据竞争会在TSan下暴露。 std::cout << "Test passed. Final value: " << shared_value << ", Total writes: " << write_increments << ", Read checksum: " << read_checksum << std::endl; }实现一个正确的读写锁是深入理解多线程同步的绝佳练习。它迫使你去思考状态、条件、唤醒、公平性这些核心概念。然而,在真实项目中,我的建议始终是:优先使用标准库std::shared_mutex,除非你有压倒性的理由不这么做。自己实现的锁,其正确性和性能需要经过极其严格的测试和验证,这个成本往往被低估。把精力花在如何用好锁,设计更合理的数据结构和并发架构上,收益会大得多。