高并发内存池(11)-PageCache获取Span(下)
高并发内存池(11)-PageCache获取Span(下)
这里在向页缓存申请新Span之前释放中心缓存的锁,是一个非常重要的性能优化策略,目的是减少锁的持有范围,提高并发性能。
为什么需要在这里解锁?
1. 避免长时间持有锁
list._mtx.unlock(); // 释放中心缓存锁PageCache::GetInstance()->_pageMtx.lock();
Span* span = PageCache::GetInstance()->NewSpan(...);
PageCache::GetInstance()->_pageMtx.unlock();
关键原因:向PageCache申请内存可能是一个相对耗时的操作,因为可能涉及:
- 遍历多个Span链表
- 分割大Span为小Span
- 甚至向操作系统申请新内存
如果持有中心缓存锁进行这些操作,会阻塞其他线程访问同一个桶。
2. 锁的粒度优化
优化前(错误做法):
// 持有中心缓存锁进行耗时操作
list._mtx.lock();
// ... 可能耗时的PageCache操作
list._mtx.unlock();
优化后(正确做法):
// 只持有锁进行快速操作
list._mtx.lock();
// 快速检查本地Span链表
list._mtx.unlock(); // 立即释放锁// 执行耗时操作(不持有中心缓存锁)
// ...// 重新加锁进行快速操作
list._mtx.lock();
// 快速插入新Span
list._mtx.unlock();
3. 减少死锁风险
PageCache有自己的锁(_pageMtx
),如果同时持有多个锁:
// 危险:可能持有多个锁
list._mtx.lock(); // 持有中心缓存锁
_pageMtx.lock(); // 持有页缓存锁(可能产生死锁)
释放中心缓存锁后再申请页缓存锁,避免了锁嵌套和潜在的死锁风险。
可视化过程
时间线对比
不释放锁的情况:
线程1: [持有锁] → [PageCache操作] → [释放锁] ⏳
线程2: [等待锁] -----------------------→ [获取锁] ❌ 长时间阻塞
释放锁的情况:
线程1: [持有锁] → [释放锁] → [PageCache操作] → [重新持锁] → [释放锁] ✅
线程2: [等待锁] → [获取锁] → [操作] → [释放锁] ✅ 几乎无阻塞
安全性考虑
你可能会担心:释放锁后,其他线程会不会修改状态?
实际上这是安全的,因为:
- 检查已完成:我们已经检查过当前桶没有可用Span
- 操作原子性:即使其他线程在此期间释放了内存,我们重新申请Span也是正确的
- 最终一致性:最后我们会将新Span加入链表,整体状态仍然一致
性能影响
这种优化在高并发场景下效果显著:
- 低竞争环境:可能影响不大
- 高竞争环境:吞吐量提升可达30-50%
- 极端情况:避免线程长时间阻塞导致的性能悬崖
总结
这个解锁操作体现了多线程编程的重要原则:
持有锁的时间应该尽可能短
通过精细的锁粒度控制,在保证线程安全的前提下,最大化并发性能。这是高性能内存分配器设计的精髓之一。