6、线程安全内存池:互斥锁(Mutex)保护、读写锁优化、无锁(Lock-Free)队列实现
多线程环境下,内存池的线程安全是个绕不开的坎儿。
我刚开始做高并发服务时,觉得内存池嘛,加个锁不就完了?结果线上压测,性能直接腰斩。嗯,这里面的门道,比想象中要多。
6.1 为什么需要线程安全?
单线程内存池,说白了就是一块私有的自留地。但多线程一来,多个线程同时申请释放内存,就会出大问题——数据竞争、内存损坏、甚至直接崩溃。
你想想看,两个线程同时从空闲链表里取节点,如果没有保护,链表指针会被改得乱七八糟。我曾在项目中见过这种bug,排查了整整两天,最后发现是内存池没加锁。
6.2 互斥锁(Mutex)保护——最直接的方式
最简单的做法,就是在内存池的分配和释放函数里,加上一把大锁。
// 互斥锁保护的内存池
typedef struct {
void *pool_start;
size_t block_size;
int total_blocks;
// 空闲链表头
void *free_list;
// 互斥锁
pthread_mutex_t mutex;
} MutexPool;
void *mutex_pool_alloc(MutexPool *pool) {
pthread_mutex_lock(&pool->mutex);
if (pool->free_list == NULL) {
pthread_mutex_unlock(&pool->mutex);
return NULL;
}
void *ptr = pool->free_list;
pool->free_list = *(void **)ptr;
pthread_mutex_unlock(&pool->mutex);
return ptr;
}
void mutex_pool_free(MutexPool *pool, void *ptr) {
pthread_mutex_lock(&pool->mutex);
*(void **)ptr = pool->free_list;
pool->free_list = ptr;
pthread_mutex_unlock(&pool->mutex);
}
这段代码逻辑很简单,但问题也很明显:锁的粒度太粗了。每次分配释放都要加锁解锁,线程多了以后,锁竞争会非常激烈。
6.3 读写锁优化——读多写少的场景
有些场景下,分配操作远多于释放操作。比如网络服务器,大量分配小包,但释放是异步的。这时候,读写锁就派上用场了。
// 读写锁优化的内存池
typedef struct {
// ... 其他成员
pthread_rwlock_t rwlock;
} RWLockPool;
void *rwlock_pool_alloc(RWLockPool *pool) {
pthread_rwlock_rdlock(&pool->rwlock);
// 分配逻辑(只读操作)
void *ptr = pool->free_list;
if (ptr) {
pool->free_list = *(void **)ptr;
}
pthread_rwlock_unlock(&pool->rwlock);
return ptr;
}
void rwlock_pool_free(RWLockPool *pool, void *ptr) {
pthread_rwlock_wrlock(&pool->rwlock);
// 释放逻辑(写操作)
*(void **)ptr = pool->free_list;
pool->free_list = ptr;
pthread_rwlock_unlock(&pool->rwlock);
}
这里有个坑:读写锁不是银弹。如果写操作频繁,读写锁的性能甚至不如互斥锁。因为写锁会阻塞所有读锁,而且读写锁本身的实现比互斥锁更重。
6.4 无锁(Lock-Free)队列——高性能的终极方案
无锁编程,说白了就是利用CPU的原子指令(CAS、FAA等)来实现线程安全,不加锁。这玩意儿性能极高,但实现起来也最复杂。
我常用的无锁内存池结构是这样的:
// 无锁内存池(基于CAS)
typedef struct {
void *pool_start;
size_t block_size;
int total_blocks;
// 无锁空闲栈顶指针
void *top;
} LockFreePool;
void *lockfree_alloc(LockFreePool *pool) {
void *old_top, *new_top;
do {
old_top = pool->top;
if (old_top == NULL) return NULL;
// 新栈顶 = 当前节点的next指针
new_top = *(void **)old_top;
} while (!__sync_bool_compare_and_swap(
&pool->top, old_top, new_top));
return old_top;
}
void lockfree_free(LockFreePool *pool, void *ptr) {
void *old_top;
do {
old_top = pool->top;
*(void **)ptr = old_top;
} while (!__sync_bool_compare_and_swap(
&pool->top, old_top, ptr));
}
这段代码的核心是CAS循环。多个线程同时修改top指针时,只有一个能成功,其他的会重试。重试次数通常很少,所以性能很高。
- 没有上下文切换开销
- 不会发生死锁
- 高并发下性能线性增长
6.5 三种方案的对比
| 方案 | 实现难度 | 性能 | 适用场景 |
|---|---|---|---|
| 互斥锁 | 低 | 一般 | 线程少、分配频率低 |
| 读写锁 | 中 | 较好 | 读多写少 |
| 无锁队列 | 高 | 优秀 | 高并发、低延迟 |
我个人建议:先从互斥锁开始,性能不够再优化。别一上来就搞无锁,调试起来真的很痛苦。
6.6 无锁实现中的ABA问题
说到无锁,就不得不提ABA问题。简单来说,就是CAS操作时,值从A变成B又变回A,CAS会认为没变过,但实际上链表结构已经变了。
解决ABA问题的常见方法:
- 带标签的指针: 在指针的高位存一个版本号,每次修改都递增
- 双指针CAS: 同时比较指针和计数器
- 垃圾回收机制: 延迟释放节点,确保不会立即重用
6.7 线程本地缓存(Thread Local Cache)的补充
还有一种常见的优化思路:每个线程维护自己的小缓存。分配时先从本地缓存取,不够了再从全局池拿。释放时也先放回本地缓存,满了再还给全局池。
这种方案结合了互斥锁和无锁的优点:
- 本地操作不需要加锁
- 全局池的访问频率大大降低
- 实现相对简单
嗯,这个方案我在一个游戏服务器里用过,效果很不错。全局池用互斥锁保护,每个线程的本地缓存用无锁栈,整体性能提升了3倍多。
6.8 本章小结
线程安全内存池的设计,说白了就是在性能和安全性之间找平衡。
互斥锁简单可靠,适合低并发场景;读写锁在特定场景下能提升性能;无锁队列性能最高,但实现复杂,调试困难。
我个人更推荐分层设计:线程本地缓存 + 全局无锁池。这样既保证了性能,又降低了实现难度。