A03 —— 多线程 malloc:arena 竞争和 per-thread arena

金句:多线程下每一次 malloc 都是一次潜在的竞争——ptmalloc 用 arena 来解决这个问题。


多线程 malloc 的问题

单线程程序中,malloc 的竞争很少(只有一个堆)。但多线程程序中,所有线程共享同一个堆,如果不做特殊处理,会出现严重的锁竞争。

Bash
多线程 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 的概念

Bash
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(有锁)

Bash
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+)

Bash
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 竞争的实际影响

Bash
观察 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 缓存

Bash
高并发场景的问题:

如果线程数 > 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不着急」,回复”资料”获取内存学习路线图。

最后修改: 2024年2月6日

作者

评论

发表评论

您的邮箱地址不会被公开。