A03 —— 多线程 malloc:arena 竞争和 per-thread arena
金句:多线程下每一次 malloc 都是一次潜在的竞争——ptmalloc 用 arena 来解决这个问题。
多线程 malloc 的问题
单线程程序中,malloc 的竞争很少(只有一个堆)。但多线程程序中,所有线程共享同一个堆,如果不做特殊处理,会出现严重的锁竞争。
多线程 malloc 的问题:
线程 A:malloc(100) → 加锁 → 分配 → 解锁
线程 B:malloc(200) → 等锁 → 等锁 → 分配 → 解锁
线程 C:malloc(300) → 等锁 → 等锁 → 等锁 → 分配 → 解锁
问题:
- 锁竞争严重(所有线程争一把锁)
- 上下文切换开销大
- 分配变成瓶颈(CPU 浪费在等锁上)
解决:per-thread arena(每个线程有自己的 arena)
线程 A:malloc(100) → 从自己的 arena 分配 → 无锁!
线程 B:malloc(200) → 从自己的 arena 分配 → 无锁!
线程 C:malloc(300) → 从自己的 arena 分配 → 无锁!Arena 的概念
Arena = 内存区域 + bin 结构 + 锁
glibc 的 arena 策略:
1. Main arena:
- 主线程使用
- 通过 brk 扩展堆
- 有一把锁(所有线程竞争)
2. Per-thread arena:
- 其他线程使用
- 通过 mmap 分配自己的 arena
- 可能有多把锁(减少竞争)
Arena 的数量限制:
- 32-bit:arena 数量 = CPU 核心数 × 2 + 1
- 64-bit:arena 数量 = CPU 核心数 × 8 + 1
超过限制后,新线程复用已有 arena(有锁)Arena 的结构(简化):
struct malloc_state {
mutex_t mutex; // 锁
int max_fast; // fastbin 大小上限
size_t top_chunk; // top chunk
// fastbins(无锁)
mfastbinptr fastbinsY[NFASTBINS];
// smallbins + largebins(需要锁)
malloc_chunk *bins[2 * NBINS];
// unsorted bin(需要锁)
malloc_chunk *bins[2];
};
每个 arena 都有自己的 bin 结构
线程优先用自己的 arena,只有满了才用其他的Thread Cache(glibc 2.26+)
Tcache(Thread Cache)= per-thread 的快速缓存
glibc 2.26 引入了 tcache,进一步减少锁竞争:
struct tcache_perthread_state {
unsigned int counts[TCACHE_MAX_BINS];
char *entries[TCACHE_MAX_BINS]; // 每种大小的链表头
};
TCACHE 的设计:
- 每个线程有自己的 tcache(无锁)
- 大小类别:32, 64, 96, 128, 192, 256, ... 1036 bytes(max 64 类)
- 每个类别最多 7 个 chunk(限制)
- 超过 7 个,放入 arena 的 bin(有锁)
分配流程(新版 glibc):
1. 检查 size 对应的 tcache
2. 如果 tcache 有 → 无锁分配(O(1))
3. 如果 tcache 空 → 进入 arena(有锁)
4. 或者合并 fastbin 到 tcache,再从 tcache 取
Tcache 的优势:
- 大部分分配(几十字节~几百字节)完全无锁
- 性能大幅提升(接近 tcmalloc)Arena 竞争的实际影响
观察 arena 竞争:
$ cat /proc/$(pidof myprogram)/maps | grep "[heap]"
00...-00... [heap] ← main arena(主堆)
00...-00... [heap] ← thread arena 1
00...-00... [heap] ← thread arena 2
...(多个 [heap] 区域)
每个 [heap] 是一个独立的 arena(mmap 分配)
Arena 数量计算(8-core 64-bit 系统):
max arenas = 8 × 8 + 1 = 65
性能对比(多线程):
- 无 tcache:锁竞争严重,malloc 耗时 100~500ns
- 有 tcache:大部分无锁,malloc 耗时 10~20ns
- tcmalloc:类似的 thread local 缓存高并发场景的问题:
如果线程数 > max arenas:
- 新线程复用已有 arena
- 锁竞争重新出现
- 性能可能比少线程更差
解决方案:
1. 减少线程数
2. 使用 tcmalloc/jemalloc(更高效的 arena 管理)
3. 使用 mmap_large(减少 mmap 调用)
perf 观察:
$ perf stat -e lock:internal_lock_acquired ./program
观察锁竞争事件的次数总结
- Arena 竞争:多线程共享堆 → 所有线程争一把锁 → 性能差
- per-thread arena:每个线程有自己的 arena(减少锁竞争)
- Arena 数量限制:32-bit = 核心×2+1,64-bit = 核心×8+1
- Tcache(glibc 2.26+):per-thread 的快速缓存,64 类大小,最多 7 个/chunk,完全无锁
- 分配流程:tcache → arena → top chunk → brk/mmap
下篇预告(A04):什么是内存泄漏?malloc 不 free 怎么导致泄漏,以及怎么检测和防止。
关注公众号「AI不着急」,回复”资料”获取内存学习路线图。
评论