p0_C++_Primer
1 | //===----------------------------------------------------------------------===// |
好的,我帮你把今天围绕 CountMinSketch 项目 的学习内容、问题、解决方案和收获做一个系统总结。内容会分模块、问题、解决方案、学到的知识点和代码规范建议。
1️⃣ 主要问题回顾
1. 插入(Insert)函数的并发安全
问题:最初尝试给整个函数加锁或用
std::vector<std::shared_lock>来一次性锁住多行。风险:可能出现死锁,尤其是两个线程交叉锁不同的行:
- 线程 A 锁行 0 → 等待行 1
- 线程 B 锁行 1 → 等待行 0
→ 典型死锁。
解决方案:
- 每次只锁当前行
std::unique_lock<std::shared_mutex> lock(*row_locks_[i]); - 操作完立即释放锁,下一行再单独加锁
- 这样即便多线程并发,每行只锁一次,不会交叉等待 → 无死锁。
- 每次只锁当前行
收获:
- 理解了死锁产生的条件:多线程、多锁、交叉等待、锁不释放
- 学会了分粒度锁设计:尽量缩小锁的作用域,提高并发性能。
2. Merge 函数的锁
问题:合并两个 CountMinSketch,需要锁住两份数据的所有行。
解决方案:
固定顺序锁定两份 sketch 的行,保证全局顺序一致:
1
2
3const CountMinSketch* first = this;
const CountMinSketch* second = &other;
if (first > second) std::swap(first, second);使用
std::vector<std::unique_lock<std::shared_mutex>> locks;来按顺序锁行,避免死锁。
收获:
- 学会了 多对象多行锁的死锁预防策略:按照地址或固定顺序锁,保证线程之间一致。
- 学会了在并发操作中锁的顺序和粒度的重要性。
3. TopK 和 Count 函数的锁
问题:
- TopK 要统计多行的频率,需要对行加读锁。
- 如果 TopK 用写锁,插入会被阻塞,性能下降。
解决方案:
- 使用
std::shared_lock<std::shared_mutex>对每行加读锁 - 插入时用写锁,TopK/Count 时用读锁 → 读写分离。
- 使用
收获:
学会了 读写锁(shared_mutex) 的使用:
std::unique_lock→ 写锁std::shared_lock→ 读锁
能够实现高并发读、独占写的设计。
4. std::vector 和 unique_ptr 使用问题
问题:
- row_locks_ 用
std::vector<std::shared_mutex>会无法拷贝/移动 - emplace_back 对
{}的推导问题
- row_locks_ 用
解决方案:
- 使用
std::vector<std::unique_ptr<std::shared_mutex>> row_locks_; - 对每行锁做
make_unique初始化 - 使用时
*row_locks_[i]获取原 mutex
- 使用
收获:
- 理解了 指针容器 vs 对象容器 在移动语义和多态上的差别
- 理解了
emplace_back和push_back在模板推导上的细微差异
5. Clear 函数
问题:
- 清零表格需要锁吗?
解决方案:
如果 Clear 会在多线程使用中调用:
- 需要给每行加写锁
- 或者全局写锁
否则单线程调用无需锁
收获:
- 理解了 锁的作用域,锁不只是保护“写操作”,还保护“数据一致性”
6. 并发性能问题
问题:
- ContentRatioTest 测试多线程插入速度
- 你的版本多线程比单线程还慢(0.91x)
原因:
Insert中每次循环都加/释放写锁,导致锁频繁申请和释放,开销大- 锁粒度还可以优化,比如 批量锁或者减少重复计算
收获:
高并发结构不仅要安全,还要考虑锁开销和热点行竞争
设计思路:
- 粒度锁:每行独立
- 避免重复加锁
- 读写分离
2️⃣ 今天学到的新知识点
死锁的产生条件
- 多线程 + 多锁 + 锁顺序不一致 + 未释放 → 死锁
- 解决:单锁粒度、固定顺序、及时释放
读写锁
shared_mutexstd::unique_lock<std::shared_mutex>→ 写锁std::shared_lock<std::shared_mutex>→ 读锁- 可提高读多写少场景性能
智能指针容器
std::vector<std::unique_ptr<T>>支持移动,避免对象拷贝/拷贝构造问题- 对象容器 vs 指针容器在 C++ 中的区别
Lock 的粒度设计
- 单行锁 > 全局锁,避免死锁,提高并发
- Lock 的作用域要尽量小,减少锁持有时间
TopK 实现
- 先统计 Count → 排序 → 截取前 k
- 注意锁住行,保证统计一致
并发性能调优
- 了解了锁申请/释放开销
- 多线程不一定快,需要减少竞争和锁开销
3️⃣ 代码规范/建议
锁作用域
1
2
3
4{
std::unique_lock<std::shared_mutex> lock(*row_locks_[i]);
// 只操作这一行
} // 离开作用域立即释放锁容器使用
std::vector<std::unique_ptr<std::shared_mutex>>推荐用于不可拷贝对象std::vector<std::pair<KeyType,uint32_t>>使用emplace_back或push_back
循环内不要重复加锁
- 如果只锁一行就操作一行,避免在循环中多余锁
读写锁分离
- 插入 → 写锁
- 查询 → 读锁
- 避免读阻塞写,写阻塞读
Merge 等操作顺序固定
- 对多个对象操作时,按地址/固定顺序加锁,防止死锁
初始化写法
1
row_locks_[i] = std::make_unique<std::shared_mutex>();
4️⃣ 总结
今天你在 CountMinSketch 项目里主要学到:
- 多线程安全设计:锁粒度、锁顺序、死锁预防
- C++ 并发工具:
unique_lock,shared_lock,shared_mutex - 智能指针和容器结合:
unique_ptr+vector - 性能意识:并发不仅安全,还要考虑锁竞争和开销
- 代码规范:锁作用域小、顺序固定、读写分离、容器初始化安全
今天你解决了“死锁风险”和“并发性能低”问题,并对 CountMinSketch 这种并发数据结构的设计原理有了深入理解。
如果你愿意,我可以帮你画一张 Insert 函数的多线程锁时序图,直观显示为什么不会死锁,也方便你复盘今天的锁设计思路。
你希望我画吗?
This work is published by lorixyu at 2026-01-13 22:21:05
Link: p0_C++_Primer
This work is licensed under CC BY-NC-SA 4.0. Please indicate lorixyu when reprinting.
